Autarch/web/templates/loadtest.html

458 lines
22 KiB
HTML
Raw Permalink Normal View History

{% extends "base.html" %}
{% block title %}Load Test - AUTARCH{% endblock %}
{% block content %}
<div class="page-header">
<h1>Load Testing</h1>
</div>
<!-- Test Controls -->
<div class="section" id="controls-section">
<h2>Test Configuration</h2>
<div class="tab-bar">
<button class="tab active" data-tab-group="lt-type" data-tab="http" onclick="showTab('lt-type','http')">HTTP Flood</button>
<button class="tab" data-tab-group="lt-type" data-tab="slowloris" onclick="showTab('lt-type','slowloris')">Slowloris</button>
<button class="tab" data-tab-group="lt-type" data-tab="tcp" onclick="showTab('lt-type','tcp')">TCP Connect</button>
<button class="tab" data-tab-group="lt-type" data-tab="syn" onclick="showTab('lt-type','syn')">SYN Flood</button>
<button class="tab" data-tab-group="lt-type" data-tab="udp" onclick="showTab('lt-type','udp')">UDP Flood</button>
</div>
<!-- HTTP Flood -->
<div class="tab-content active" data-tab-group="lt-type" data-tab="http">
<div style="display:grid;grid-template-columns:1fr auto;gap:0.75rem;align-items:end;margin-top:0.75rem">
<div class="form-group" style="margin-bottom:0">
<label for="lt-target">Target URL</label>
<input type="text" id="lt-target" placeholder="https://example.com/api/endpoint">
</div>
<div class="form-group" style="margin-bottom:0">
<label for="lt-method">Method</label>
<select id="lt-method" style="width:90px">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="HEAD">HEAD</option>
<option value="PATCH">PATCH</option>
</select>
</div>
</div>
<div class="form-group" style="margin-top:0.5rem">
<label for="lt-headers">Custom Headers (JSON)</label>
<input type="text" id="lt-headers" placeholder='{"Authorization":"Bearer token","Content-Type":"application/json"}'>
</div>
<div class="form-group">
<label for="lt-body">Request Body</label>
<input type="text" id="lt-body" placeholder='{"key":"value"} or form data'>
</div>
<div style="display:flex;gap:0.75rem;flex-wrap:wrap">
<label style="font-size:0.85rem;display:flex;align-items:center;gap:0.3rem;cursor:pointer">
<input type="checkbox" id="lt-follow-redirects" checked> Follow redirects
</label>
<label style="font-size:0.85rem;display:flex;align-items:center;gap:0.3rem;cursor:pointer">
<input type="checkbox" id="lt-verify-ssl"> Verify SSL
</label>
<label style="font-size:0.85rem;display:flex;align-items:center;gap:0.3rem;cursor:pointer">
<input type="checkbox" id="lt-rotate-ua" checked> Rotate User-Agents
</label>
</div>
</div>
<!-- Slowloris -->
<div class="tab-content" data-tab-group="lt-type" data-tab="slowloris">
<div style="margin-top:0.75rem">
<div class="form-group">
<label for="lt-slowloris-target">Target URL or host:port</label>
<input type="text" id="lt-slowloris-target" placeholder="https://example.com or 192.168.1.1:80">
</div>
</div>
<p style="font-size:0.8rem;color:var(--text-muted)">Slowloris holds connections open with partial HTTP headers, exhausting the target's connection pool. Each worker manages ~50 sockets.</p>
</div>
<!-- TCP Connect -->
<div class="tab-content" data-tab-group="lt-type" data-tab="tcp">
<div style="display:grid;grid-template-columns:1fr auto;gap:0.75rem;align-items:end;margin-top:0.75rem">
<div class="form-group" style="margin-bottom:0">
<label for="lt-tcp-target">Target (host:port)</label>
<input type="text" id="lt-tcp-target" placeholder="192.168.1.1:80">
</div>
<div class="form-group" style="margin-bottom:0">
<label for="lt-tcp-payload">Payload (bytes)</label>
<input type="number" id="lt-tcp-payload" value="0" style="width:90px" min="0" max="65535">
</div>
</div>
<p style="font-size:0.8rem;color:var(--text-muted);margin-top:0.5rem">Rapid TCP connect/disconnect to exhaust server resources. Set payload &gt; 0 to send random data per connection.</p>
</div>
<!-- SYN Flood -->
<div class="tab-content" data-tab-group="lt-type" data-tab="syn">
<div style="display:grid;grid-template-columns:1fr auto;gap:0.75rem;align-items:end;margin-top:0.75rem">
<div class="form-group" style="margin-bottom:0">
<label for="lt-syn-target">Target (host:port)</label>
<input type="text" id="lt-syn-target" placeholder="192.168.1.1:80">
</div>
<div class="form-group" style="margin-bottom:0">
<label for="lt-syn-srcip">Source IP (optional)</label>
<input type="text" id="lt-syn-srcip" placeholder="auto-detect" style="width:140px">
</div>
</div>
<p style="font-size:0.8rem;color:var(--warning);margin-top:0.5rem">Requires administrator/root privileges for raw sockets. Falls back to TCP connect flood without admin.</p>
</div>
<!-- UDP Flood -->
<div class="tab-content" data-tab-group="lt-type" data-tab="udp">
<div style="display:grid;grid-template-columns:1fr auto;gap:0.75rem;align-items:end;margin-top:0.75rem">
<div class="form-group" style="margin-bottom:0">
<label for="lt-udp-target">Target (host:port)</label>
<input type="text" id="lt-udp-target" placeholder="192.168.1.1:53">
</div>
<div class="form-group" style="margin-bottom:0">
<label for="lt-udp-payload">Payload (bytes)</label>
<input type="number" id="lt-udp-payload" value="1024" style="width:90px" min="1" max="65535">
</div>
</div>
<p style="font-size:0.8rem;color:var(--text-muted);margin-top:0.5rem">Sends UDP packets at maximum rate. Effective against UDP services (DNS, NTP, etc.).</p>
</div>
<!-- Common settings -->
<div style="margin-top:1rem;padding-top:0.75rem;border-top:1px solid var(--border)">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:0.75rem;align-items:end">
<div class="form-group" style="margin-bottom:0">
<label for="lt-workers">Workers</label>
<input type="number" id="lt-workers" value="10" min="1" max="500">
</div>
<div class="form-group" style="margin-bottom:0">
<label for="lt-duration">Duration (s)</label>
<input type="number" id="lt-duration" value="30" min="0">
</div>
<div class="form-group" style="margin-bottom:0">
<label for="lt-ramp">Ramp Pattern</label>
<select id="lt-ramp">
<option value="constant">Constant</option>
<option value="linear">Linear Ramp</option>
<option value="step">Step</option>
<option value="spike">Spike</option>
</select>
</div>
<div class="form-group" style="margin-bottom:0">
<label for="lt-ramp-dur">Ramp Time (s)</label>
<input type="number" id="lt-ramp-dur" value="0" min="0">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:0.75rem;align-items:end;margin-top:0.5rem">
<div class="form-group" style="margin-bottom:0">
<label for="lt-rate">Rate Limit (req/s)</label>
<input type="number" id="lt-rate" value="0" min="0" placeholder="0 = unlimited">
</div>
<div class="form-group" style="margin-bottom:0">
<label for="lt-timeout">Timeout (s)</label>
<input type="number" id="lt-timeout" value="10" min="1">
</div>
<div class="form-group" style="margin-bottom:0">
<label for="lt-max-req">Max Req/Worker</label>
<input type="number" id="lt-max-req" value="0" min="0" placeholder="0 = unlimited">
</div>
<div></div>
</div>
</div>
<!-- Launch controls -->
<div class="tool-actions" style="margin-top:1rem">
<button id="btn-start" class="btn btn-danger" onclick="startTest()">Start Test</button>
<button id="btn-pause" class="btn btn-secondary" onclick="pauseTest()" style="display:none">Pause</button>
<button id="btn-resume" class="btn btn-primary" onclick="resumeTest()" style="display:none">Resume</button>
<button id="btn-stop" class="btn btn-secondary" onclick="stopTest()" style="display:none">Stop</button>
</div>
</div>
<!-- Live Dashboard -->
<div class="section" id="dashboard-section" style="display:none">
<h2>Live Metrics</h2>
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:0.75rem">
<div class="card" style="text-align:center">
<div style="font-size:1.8rem;font-weight:700;color:var(--accent)" id="m-rps">0</div>
<div style="font-size:0.75rem;color:var(--text-muted)">Requests/sec</div>
</div>
<div class="card" style="text-align:center">
<div style="font-size:1.8rem;font-weight:700" id="m-total">0</div>
<div style="font-size:0.75rem;color:var(--text-muted)">Total Requests</div>
</div>
<div class="card" style="text-align:center">
<div style="font-size:1.8rem;font-weight:700;color:var(--success)" id="m-success">0%</div>
<div style="font-size:0.75rem;color:var(--text-muted)">Success Rate</div>
</div>
<div class="card" style="text-align:center">
<div style="font-size:1.8rem;font-weight:700;color:var(--warning)" id="m-avg-latency">0ms</div>
<div style="font-size:0.75rem;color:var(--text-muted)">Avg Latency</div>
</div>
</div>
<!-- Detail metrics -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;margin-top:0.75rem">
<div class="card">
<h3 style="margin:0 0 0.5rem 0;font-size:0.9rem">Performance</h3>
<table style="width:100%;font-size:0.85rem">
<tr><td style="color:var(--text-muted)">Workers</td><td id="m-workers" style="text-align:right">0</td></tr>
<tr><td style="color:var(--text-muted)">Elapsed</td><td id="m-elapsed" style="text-align:right">0s</td></tr>
<tr><td style="color:var(--text-muted)">Successful</td><td id="m-ok" style="text-align:right;color:var(--success)">0</td></tr>
<tr><td style="color:var(--text-muted)">Failed</td><td id="m-fail" style="text-align:right;color:var(--error)">0</td></tr>
<tr><td style="color:var(--text-muted)">Data Sent</td><td id="m-sent" style="text-align:right">0</td></tr>
<tr><td style="color:var(--text-muted)">Data Recv</td><td id="m-recv" style="text-align:right">0</td></tr>
</table>
</div>
<div class="card">
<h3 style="margin:0 0 0.5rem 0;font-size:0.9rem">Latency Percentiles</h3>
<table style="width:100%;font-size:0.85rem">
<tr><td style="color:var(--text-muted)">Min</td><td id="m-p-min" style="text-align:right">0ms</td></tr>
<tr><td style="color:var(--text-muted)">P50 (Median)</td><td id="m-p50" style="text-align:right">0ms</td></tr>
<tr><td style="color:var(--text-muted)">P95</td><td id="m-p95" style="text-align:right">0ms</td></tr>
<tr><td style="color:var(--text-muted)">P99</td><td id="m-p99" style="text-align:right">0ms</td></tr>
<tr><td style="color:var(--text-muted)">Max</td><td id="m-p-max" style="text-align:right">0ms</td></tr>
<tr><td style="color:var(--text-muted)">Error Rate</td><td id="m-err-rate" style="text-align:right">0%</td></tr>
</table>
</div>
</div>
<!-- RPS chart (ASCII-style bar chart) -->
<div class="card" style="margin-top:0.75rem">
<h3 style="margin:0 0 0.5rem 0;font-size:0.9rem">RPS Over Time</h3>
<div id="rps-chart" style="height:80px;display:flex;align-items:flex-end;gap:1px;overflow:hidden"></div>
</div>
<!-- Status codes & errors -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;margin-top:0.75rem">
<div class="card">
<h3 style="margin:0 0 0.5rem 0;font-size:0.9rem">Status Codes</h3>
<div id="m-status-codes" style="font-size:0.85rem"><span style="color:var(--text-muted)"></span></div>
</div>
<div class="card">
<h3 style="margin:0 0 0.5rem 0;font-size:0.9rem">Top Errors</h3>
<div id="m-errors" style="font-size:0.85rem"><span style="color:var(--text-muted)"></span></div>
</div>
</div>
</div>
<!-- Presets -->
<div class="section">
<h2>Quick Presets</h2>
<div style="display:grid;grid-template-columns:repeat(3, 1fr);gap:0.75rem">
<button class="btn btn-small" onclick="applyPreset('gentle')">Gentle (5w / 10s)</button>
<button class="btn btn-small" onclick="applyPreset('moderate')">Moderate (25w / 30s)</button>
<button class="btn btn-small" onclick="applyPreset('heavy')">Heavy (100w / 60s)</button>
<button class="btn btn-small" onclick="applyPreset('stress')">Stress (250w / 120s)</button>
<button class="btn btn-small" onclick="applyPreset('ramp')">Linear Ramp (50w / 60s)</button>
<button class="btn btn-small" onclick="applyPreset('spike')">Spike Test (100w / 30s)</button>
</div>
</div>
<script>
let _ltEventSource = null;
let _ltPollTimer = null;
function _v(id) { const el = document.getElementById(id); return el ? el.value.trim() : ''; }
function _getActiveAttackType() {
const tabs = document.querySelectorAll('[data-tab-group="lt-type"].tab-content');
for (const t of tabs) {
if (t.classList.contains('active')) return t.dataset.tab;
}
return 'http';
}
function _buildConfig() {
const type = _getActiveAttackType();
const config = {
workers: parseInt(_v('lt-workers')) || 10,
duration: parseInt(_v('lt-duration')) || 30,
ramp_pattern: _v('lt-ramp') || 'constant',
ramp_duration: parseInt(_v('lt-ramp-dur')) || 0,
rate_limit: parseInt(_v('lt-rate')) || 0,
timeout: parseInt(_v('lt-timeout')) || 10,
requests_per_worker: parseInt(_v('lt-max-req')) || 0,
};
if (type === 'http') {
config.attack_type = 'http_flood';
config.target = _v('lt-target');
config.method = _v('lt-method') || 'GET';
config.body = _v('lt-body') || '';
config.follow_redirects = document.getElementById('lt-follow-redirects').checked;
config.verify_ssl = document.getElementById('lt-verify-ssl').checked;
config.rotate_useragent = document.getElementById('lt-rotate-ua').checked;
try { config.headers = JSON.parse(_v('lt-headers') || '{}'); } catch(e) { config.headers = {}; }
} else if (type === 'slowloris') {
config.attack_type = 'slowloris';
config.target = _v('lt-slowloris-target');
} else if (type === 'tcp') {
config.attack_type = 'tcp_connect';
config.target = _v('lt-tcp-target');
config.payload_size = parseInt(_v('lt-tcp-payload')) || 0;
} else if (type === 'syn') {
config.attack_type = 'syn_flood';
config.target = _v('lt-syn-target');
config.source_ip = _v('lt-syn-srcip') || '';
} else if (type === 'udp') {
config.attack_type = 'udp_flood';
config.target = _v('lt-udp-target');
config.payload_size = parseInt(_v('lt-udp-payload')) || 1024;
}
return config;
}
function startTest() {
const config = _buildConfig();
if (!config.target) { alert('Enter a target'); return; }
fetch('/loadtest/start', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
}).then(r => r.json()).then(d => {
if (!d.ok) { alert(d.error || 'Failed to start'); return; }
_showRunning();
_startStream();
}).catch(e => alert('Error: ' + e.message));
}
function stopTest() {
fetch('/loadtest/stop', {method: 'POST'}).then(() => {
_showStopped();
_stopStream();
});
}
function pauseTest() {
fetch('/loadtest/pause', {method: 'POST'}).then(() => {
document.getElementById('btn-pause').style.display = 'none';
document.getElementById('btn-resume').style.display = '';
});
}
function resumeTest() {
fetch('/loadtest/resume', {method: 'POST'}).then(() => {
document.getElementById('btn-resume').style.display = 'none';
document.getElementById('btn-pause').style.display = '';
});
}
function _showRunning() {
document.getElementById('btn-start').style.display = 'none';
document.getElementById('btn-pause').style.display = '';
document.getElementById('btn-stop').style.display = '';
document.getElementById('btn-resume').style.display = 'none';
document.getElementById('dashboard-section').style.display = '';
}
function _showStopped() {
document.getElementById('btn-start').style.display = '';
document.getElementById('btn-pause').style.display = 'none';
document.getElementById('btn-stop').style.display = 'none';
document.getElementById('btn-resume').style.display = 'none';
}
function _startStream() {
_stopStream();
// Use polling as a reliable fallback (SSE can be flaky through proxies)
_ltPollTimer = setInterval(function() {
fetch('/loadtest/status').then(r => r.json()).then(d => {
if (!d.running) {
_showStopped();
_stopStream();
}
if (d.metrics) _updateMetrics(d.metrics);
}).catch(() => {});
}, 1000);
}
function _stopStream() {
if (_ltEventSource) { _ltEventSource.close(); _ltEventSource = null; }
if (_ltPollTimer) { clearInterval(_ltPollTimer); _ltPollTimer = null; }
}
function _fmtBytes(b) {
if (b < 1024) return b + ' B';
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB';
return (b / 1073741824).toFixed(2) + ' GB';
}
function _updateMetrics(m) {
document.getElementById('m-rps').textContent = m.rps.toFixed(1);
document.getElementById('m-total').textContent = m.total_requests;
document.getElementById('m-success').textContent = m.success_rate.toFixed(1) + '%';
document.getElementById('m-avg-latency').textContent = m.avg_latency.toFixed(0) + 'ms';
document.getElementById('m-workers').textContent = m.active_workers;
document.getElementById('m-elapsed').textContent = m.elapsed.toFixed(0) + 's';
document.getElementById('m-ok').textContent = m.successful;
document.getElementById('m-fail').textContent = m.failed;
document.getElementById('m-sent').textContent = _fmtBytes(m.bytes_sent);
document.getElementById('m-recv').textContent = _fmtBytes(m.bytes_received);
document.getElementById('m-p-min').textContent = m.min_latency.toFixed(1) + 'ms';
document.getElementById('m-p50').textContent = m.p50_latency.toFixed(1) + 'ms';
document.getElementById('m-p95').textContent = m.p95_latency.toFixed(1) + 'ms';
document.getElementById('m-p99').textContent = m.p99_latency.toFixed(1) + 'ms';
document.getElementById('m-p-max').textContent = m.max_latency.toFixed(1) + 'ms';
document.getElementById('m-err-rate').textContent = m.error_rate.toFixed(1) + '%';
// Status codes
const codes = m.status_codes || {};
const codeKeys = Object.keys(codes).sort();
if (codeKeys.length) {
document.getElementById('m-status-codes').innerHTML = codeKeys.map(c => {
const color = c < 300 ? 'var(--success)' : c < 400 ? 'var(--warning)' : 'var(--error)';
return '<span style="color:' + color + '">' + escapeHtml(c) + '</span>: ' + codes[c];
}).join(' &nbsp; ');
}
// Errors
const errs = m.top_errors || {};
const errKeys = Object.keys(errs);
if (errKeys.length) {
document.getElementById('m-errors').innerHTML = errKeys.map(e =>
'<div>' + errs[e] + '&times; <span style="color:var(--text-muted)">' + escapeHtml(e) + '</span></div>'
).join('');
}
// RPS chart
const rpsHist = m.rps_history || [];
if (rpsHist.length > 1) {
const chart = document.getElementById('rps-chart');
const maxRps = Math.max(...rpsHist, 1);
chart.innerHTML = rpsHist.map(r => {
const h = Math.max(2, (r / maxRps) * 70);
return '<div style="flex:1;min-width:2px;background:var(--accent);height:' + h + 'px;border-radius:1px 1px 0 0" title="' + r + ' req/s"></div>';
}).join('');
}
}
function applyPreset(name) {
const presets = {
'gentle': {workers: 5, duration: 10, ramp: 'constant', rampDur: 0},
'moderate': {workers: 25, duration: 30, ramp: 'constant', rampDur: 0},
'heavy': {workers: 100, duration: 60, ramp: 'constant', rampDur: 0},
'stress': {workers: 250, duration: 120, ramp: 'constant', rampDur: 0},
'ramp': {workers: 50, duration: 60, ramp: 'linear', rampDur: 30},
'spike': {workers: 100, duration: 30, ramp: 'spike', rampDur: 10},
};
const p = presets[name];
if (!p) return;
document.getElementById('lt-workers').value = p.workers;
document.getElementById('lt-duration').value = p.duration;
document.getElementById('lt-ramp').value = p.ramp;
document.getElementById('lt-ramp-dur').value = p.rampDur;
}
// On load: check if test is already running
document.addEventListener('DOMContentLoaded', function() {
fetch('/loadtest/status').then(r => r.json()).then(d => {
if (d.running) {
_showRunning();
_startStream();
if (d.metrics) _updateMetrics(d.metrics);
}
}).catch(() => {});
});
</script>
{% endblock %}