|
@@ -41,7 +41,8 @@ let state = {
|
|
|
policyRules: [],
|
|
policyRules: [],
|
|
|
routingTables: [],
|
|
routingTables: [],
|
|
|
routingFilters: [],
|
|
routingFilters: [],
|
|
|
- filterChainSelected: 'all'
|
|
|
|
|
|
|
+ filterChainSelected: 'all',
|
|
|
|
|
+ activeToolsSubTab: 'ping'
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const API_BASE = '/api';
|
|
const API_BASE = '/api';
|
|
@@ -113,6 +114,7 @@ function navigate(tabName) {
|
|
|
ipsec: { title: 'IPSec (IKEv2) strongSwan', sub: 'Enterprise Site-to-Site & Roadwarrior IPSec controller' },
|
|
ipsec: { title: 'IPSec (IKEv2) strongSwan', sub: 'Enterprise Site-to-Site & Roadwarrior IPSec controller' },
|
|
|
l2tp: { title: 'L2TP VPN Client', sub: 'Layer 2 Tunneling Protocol via xl2tpd' },
|
|
l2tp: { title: 'L2TP VPN Client', sub: 'Layer 2 Tunneling Protocol via xl2tpd' },
|
|
|
pptp: { title: 'PPTP VPN Client', sub: 'Point-to-Point Tunneling Protocol with MPPE-128 encryption' },
|
|
pptp: { title: 'PPTP VPN Client', sub: 'Point-to-Point Tunneling Protocol with MPPE-128 encryption' },
|
|
|
|
|
+ tools: { title: 'Troubleshoot & Diagnostic Tools (/tool)', sub: 'RouterOS-style Ping, Traceroute, MTR Telemetry, DNS resolution, and TCP Port probing' },
|
|
|
persistence: { title: 'Debian Configuration', sub: 'Native /etc/network/interfaces.d/ persistence manager' }
|
|
persistence: { title: 'Debian Configuration', sub: 'Native /etc/network/interfaces.d/ persistence manager' }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -152,6 +154,7 @@ function navigate(tabName) {
|
|
|
if (tabName === 'ipsec') loadIPSec();
|
|
if (tabName === 'ipsec') loadIPSec();
|
|
|
if (tabName === 'l2tp') loadL2TP();
|
|
if (tabName === 'l2tp') loadL2TP();
|
|
|
if (tabName === 'pptp') loadPPTP();
|
|
if (tabName === 'pptp') loadPPTP();
|
|
|
|
|
+ if (tabName === 'tools') loadToolsView();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Fetch all core system, network and VPN telemetry
|
|
// Fetch all core system, network and VPN telemetry
|
|
@@ -7038,3 +7041,470 @@ async function toggleRoutingFilterState(id, disabled) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ==========================================
|
|
|
|
|
+// TROUBLESHOOT & DIAGNOSTIC TOOLS (/tool)
|
|
|
|
|
+// ==========================================
|
|
|
|
|
+
|
|
|
|
|
+function loadToolsView() {
|
|
|
|
|
+ populateToolsInterfaces();
|
|
|
|
|
+ switchToolsSubTab(state.activeToolsSubTab || 'ping');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function populateToolsInterfaces() {
|
|
|
|
|
+ const pingSel = document.getElementById('tools-ping-iface');
|
|
|
|
|
+ const traceSel = document.getElementById('tools-trace-iface');
|
|
|
|
|
+ if (!pingSel || !traceSel) return;
|
|
|
|
|
+
|
|
|
|
|
+ const currentPing = pingSel.value;
|
|
|
|
|
+ const currentTrace = traceSel.value;
|
|
|
|
|
+
|
|
|
|
|
+ let opts = '<option value="">Default (System Routing)</option>';
|
|
|
|
|
+ if (Array.isArray(state.interfaces)) {
|
|
|
|
|
+ state.interfaces.forEach(iface => {
|
|
|
|
|
+ if (iface.name) {
|
|
|
|
|
+ opts += `<option value="${escapeHtml(iface.name)}">${escapeHtml(iface.name)}${iface.ip_address ? ' (' + escapeHtml(iface.ip_address) + ')' : ''}</option>`;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pingSel.innerHTML = opts;
|
|
|
|
|
+ traceSel.innerHTML = opts;
|
|
|
|
|
+
|
|
|
|
|
+ if (currentPing) pingSel.value = currentPing;
|
|
|
|
|
+ if (currentTrace) traceSel.value = currentTrace;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function switchToolsSubTab(subTab) {
|
|
|
|
|
+ state.activeToolsSubTab = subTab;
|
|
|
|
|
+ const tabs = ['ping', 'traceroute', 'mtr', 'dns', 'port'];
|
|
|
|
|
+
|
|
|
|
|
+ tabs.forEach(t => {
|
|
|
|
|
+ const btn = document.getElementById(`tools-subtab-btn-${t}`);
|
|
|
|
|
+ const view = document.getElementById(`tools-view-${t}`);
|
|
|
|
|
+ if (btn) {
|
|
|
|
|
+ if (t === subTab) {
|
|
|
|
|
+ btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-amber-400 text-amber-300 transition flex items-center gap-2 whitespace-nowrap';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (view) {
|
|
|
|
|
+ if (t === subTab) {
|
|
|
|
|
+ view.classList.remove('hidden');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ view.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function setPortPreset(port) {
|
|
|
|
|
+ const input = document.getElementById('tools-port-num');
|
|
|
|
|
+ if (input) input.value = port;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function executePing(e) {
|
|
|
|
|
+ if (e) e.preventDefault();
|
|
|
|
|
+ const target = document.getElementById('tools-ping-target')?.value?.trim();
|
|
|
|
|
+ const count = parseInt(document.getElementById('tools-ping-count')?.value || '4', 10);
|
|
|
|
|
+ const size = parseInt(document.getElementById('tools-ping-size')?.value || '56', 10);
|
|
|
|
|
+ const timeout = parseInt(document.getElementById('tools-ping-timeout')?.value || '5', 10);
|
|
|
|
|
+ const iface = document.getElementById('tools-ping-iface')?.value || '';
|
|
|
|
|
+ const dontFrag = document.getElementById('tools-ping-df')?.checked || false;
|
|
|
|
|
+
|
|
|
|
|
+ if (!target) {
|
|
|
|
|
+ showToast('Input Required', 'Please enter a target host or IP', 'warning');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const btn = document.getElementById('tools-ping-btn');
|
|
|
|
|
+ const spinner = document.getElementById('tools-ping-spinner');
|
|
|
|
|
+ if (btn) btn.disabled = true;
|
|
|
|
|
+ if (spinner) spinner.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/tools/ping`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ target,
|
|
|
|
|
+ count,
|
|
|
|
|
+ size,
|
|
|
|
|
+ timeout,
|
|
|
|
|
+ interface: iface,
|
|
|
|
|
+ dont_frag: dontFrag
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ showToast('Ping Error', data.error || 'Failed to execute ping', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const resCard = document.getElementById('tools-ping-results');
|
|
|
|
|
+ if (resCard) resCard.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('tools-ping-stat-sent').innerText = data.packets_sent ?? 0;
|
|
|
|
|
+ document.getElementById('tools-ping-stat-rcv').innerText = data.packets_recv ?? 0;
|
|
|
|
|
+
|
|
|
|
|
+ const lossEl = document.getElementById('tools-ping-stat-loss');
|
|
|
|
|
+ if (lossEl) {
|
|
|
|
|
+ const loss = data.packet_loss_pct ?? 0;
|
|
|
|
|
+ lossEl.innerText = `${loss}%`;
|
|
|
|
|
+ lossEl.className = loss === 0 ? 'text-lg font-bold font-mono text-emerald-400' : (loss < 50 ? 'text-lg font-bold font-mono text-amber-400' : 'text-lg font-bold font-mono text-rose-400');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('tools-ping-stat-min').innerText = data.min_rtt_ms ? `${data.min_rtt_ms.toFixed(1)} ms` : '--';
|
|
|
|
|
+ document.getElementById('tools-ping-stat-avg').innerText = data.avg_rtt_ms ? `${data.avg_rtt_ms.toFixed(1)} ms` : '--';
|
|
|
|
|
+ document.getElementById('tools-ping-stat-max').innerText = data.max_rtt_ms ? `${data.max_rtt_ms.toFixed(1)} ms` : '--';
|
|
|
|
|
+ document.getElementById('tools-ping-stat-jitter').innerText = data.mdev_rtt_ms ? `${data.mdev_rtt_ms.toFixed(2)} ms` : '--';
|
|
|
|
|
+
|
|
|
|
|
+ const tbody = document.getElementById('tools-ping-table-body');
|
|
|
|
|
+ if (tbody) {
|
|
|
|
|
+ if (Array.isArray(data.replies) && data.replies.length > 0) {
|
|
|
|
|
+ tbody.innerHTML = data.replies.map(r => `
|
|
|
|
|
+ <tr class="hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs">
|
|
|
|
|
+ <td class="p-3 text-slate-300">#${r.seq}</td>
|
|
|
|
|
+ <td class="p-3 font-semibold text-white">${escapeHtml(r.from)}</td>
|
|
|
|
|
+ <td class="p-3 text-slate-400">${r.bytes} bytes</td>
|
|
|
|
|
+ <td class="p-3 text-slate-300">${r.ttl}</td>
|
|
|
|
|
+ <td class="p-3 font-bold text-amber-300">${r.time_ms.toFixed(2)} ms</td>
|
|
|
|
|
+ <td class="p-3"><span class="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-950 text-emerald-300 border border-emerald-800/60">REPLY</span></td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `).join('');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tbody.innerHTML = `<tr><td colspan="6" class="p-4 text-center text-xs text-rose-400 font-mono">No replies received (100% packet loss or timed out)</td></tr>`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const rawEl = document.getElementById('tools-ping-raw');
|
|
|
|
|
+ if (rawEl) rawEl.innerText = data.raw_output || 'No output';
|
|
|
|
|
+
|
|
|
|
|
+ showToast('Ping Finished', `Sent ${data.packets_sent}, received ${data.packets_recv} (${data.packet_loss_pct}% loss)`, data.packet_loss_pct === 0 ? 'success' : 'info');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Ping Error', err.message, 'error');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (btn) btn.disabled = false;
|
|
|
|
|
+ if (spinner) spinner.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function executeTraceroute(e) {
|
|
|
|
|
+ if (e) e.preventDefault();
|
|
|
|
|
+ const target = document.getElementById('tools-trace-target')?.value?.trim();
|
|
|
|
|
+ const maxHops = parseInt(document.getElementById('tools-trace-maxhops')?.value || '15', 10);
|
|
|
|
|
+ const timeout = parseInt(document.getElementById('tools-trace-timeout')?.value || '1', 10);
|
|
|
|
|
+ const protocol = document.getElementById('tools-trace-protocol')?.value || 'icmp';
|
|
|
|
|
+ const iface = document.getElementById('tools-trace-iface')?.value || '';
|
|
|
|
|
+ const resolve = document.getElementById('tools-trace-resolve')?.checked || false;
|
|
|
|
|
+
|
|
|
|
|
+ if (!target) {
|
|
|
|
|
+ showToast('Input Required', 'Please enter a target host or IP', 'warning');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const btn = document.getElementById('tools-trace-btn');
|
|
|
|
|
+ const spinner = document.getElementById('tools-trace-spinner');
|
|
|
|
|
+ if (btn) btn.disabled = true;
|
|
|
|
|
+ if (spinner) spinner.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/tools/traceroute`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ target,
|
|
|
|
|
+ max_hops: maxHops,
|
|
|
|
|
+ timeout,
|
|
|
|
|
+ protocol,
|
|
|
|
|
+ interface: iface,
|
|
|
|
|
+ resolve
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ showToast('Traceroute Error', data.error || 'Failed to execute traceroute', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const resCard = document.getElementById('tools-trace-results');
|
|
|
|
|
+ if (resCard) resCard.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ const hopsCount = Array.isArray(data.hops) ? data.hops.length : 0;
|
|
|
|
|
+ const statHops = document.getElementById('tools-trace-stat-hops');
|
|
|
|
|
+ if (statHops) statHops.innerText = hopsCount;
|
|
|
|
|
+
|
|
|
|
|
+ const statStatus = document.getElementById('tools-trace-stat-status');
|
|
|
|
|
+ if (statStatus) {
|
|
|
|
|
+ statStatus.innerText = hopsCount > 0 ? 'Trace Complete' : 'No Hops Discovered';
|
|
|
|
|
+ statStatus.className = hopsCount > 0 ? 'text-xs font-bold font-mono text-emerald-400' : 'text-xs font-bold font-mono text-rose-400';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const tbody = document.getElementById('tools-trace-table-body');
|
|
|
|
|
+ if (tbody) {
|
|
|
|
|
+ if (hopsCount > 0) {
|
|
|
|
|
+ tbody.innerHTML = data.hops.map(h => {
|
|
|
|
|
+ const isTimeout = h.status === 'timeout' || (!h.ip && !h.host);
|
|
|
|
|
+ const rttDisplay = Array.isArray(h.times_ms) && h.times_ms.length > 0 ? h.times_ms.join(', ') : (isTimeout ? '* * *' : '--');
|
|
|
|
|
+ return `
|
|
|
|
|
+ <tr class="hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs ${isTimeout ? 'opacity-60' : ''}">
|
|
|
|
|
+ <td class="p-3 text-slate-300 font-bold">${h.hop}</td>
|
|
|
|
|
+ <td class="p-3 font-semibold ${isTimeout ? 'text-slate-500 italic' : 'text-cyan-300'}">${escapeHtml(h.ip || (isTimeout ? 'Request timed out' : ''))}</td>
|
|
|
|
|
+ <td class="p-3 text-slate-400">${escapeHtml(h.host || '--')}</td>
|
|
|
|
|
+ <td class="p-3 font-bold ${isTimeout ? 'text-slate-500' : 'text-amber-300'}">${escapeHtml(rttDisplay)}</td>
|
|
|
|
|
+ <td class="p-3">
|
|
|
|
|
+ ${isTimeout
|
|
|
|
|
+ ? '<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-slate-800 text-slate-400 border border-slate-700">TIMEOUT</span>'
|
|
|
|
|
+ : '<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-950 text-emerald-300 border border-emerald-800/60">REACHABLE</span>'}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tbody.innerHTML = `<tr><td colspan="5" class="p-4 text-center text-xs text-rose-400 font-mono">No route or hops discovered</td></tr>`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const rawEl = document.getElementById('tools-trace-raw');
|
|
|
|
|
+ if (rawEl) rawEl.innerText = data.raw_output || 'No output';
|
|
|
|
|
+
|
|
|
|
|
+ showToast('Traceroute Finished', `Traced ${hopsCount} hops to ${target}`, 'info');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Traceroute Error', err.message, 'error');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (btn) btn.disabled = false;
|
|
|
|
|
+ if (spinner) spinner.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function executeMTR(e) {
|
|
|
|
|
+ if (e) e.preventDefault();
|
|
|
|
|
+ const target = document.getElementById('tools-mtr-target')?.value?.trim();
|
|
|
|
|
+ const cycles = parseInt(document.getElementById('tools-mtr-cycles')?.value || '3', 10);
|
|
|
|
|
+
|
|
|
|
|
+ if (!target) {
|
|
|
|
|
+ showToast('Input Required', 'Please enter a target host or IP', 'warning');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const btn = document.getElementById('tools-mtr-btn');
|
|
|
|
|
+ const spinner = document.getElementById('tools-mtr-spinner');
|
|
|
|
|
+ if (btn) btn.disabled = true;
|
|
|
|
|
+ if (spinner) spinner.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/tools/mtr`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ target, count: cycles })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ showToast('MTR Error', data.error || 'Failed to execute MTR', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const resCard = document.getElementById('tools-mtr-results');
|
|
|
|
|
+ if (resCard) resCard.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ const hopsCount = Array.isArray(data.hops) ? data.hops.length : 0;
|
|
|
|
|
+ const statHops = document.getElementById('tools-mtr-stat-hops');
|
|
|
|
|
+ if (statHops) statHops.innerText = hopsCount;
|
|
|
|
|
+
|
|
|
|
|
+ const statDest = document.getElementById('tools-mtr-stat-dest');
|
|
|
|
|
+ if (statDest) statDest.innerText = data.target || target;
|
|
|
|
|
+
|
|
|
|
|
+ const tbody = document.getElementById('tools-mtr-table-body');
|
|
|
|
|
+ if (tbody) {
|
|
|
|
|
+ if (hopsCount > 0) {
|
|
|
|
|
+ tbody.innerHTML = data.hops.map(h => {
|
|
|
|
|
+ const lossClass = h.loss_pct === 0 ? 'text-emerald-400 font-bold' : (h.loss_pct < 20 ? 'text-amber-400 font-bold' : 'text-rose-400 font-bold');
|
|
|
|
|
+ return `
|
|
|
|
|
+ <tr class="hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs">
|
|
|
|
|
+ <td class="p-3 text-slate-300 font-bold">${h.hop}</td>
|
|
|
|
|
+ <td class="p-3 font-semibold text-white">${escapeHtml(h.host || '--')}</td>
|
|
|
|
|
+ <td class="p-3 ${lossClass}">${h.loss_pct.toFixed(1)}%</td>
|
|
|
|
|
+ <td class="p-3 text-slate-300">${h.sent}</td>
|
|
|
|
|
+ <td class="p-3 text-slate-300">${h.last_ms.toFixed(1)} ms</td>
|
|
|
|
|
+ <td class="p-3 font-bold text-amber-300">${h.avg_ms.toFixed(1)} ms</td>
|
|
|
|
|
+ <td class="p-3 text-emerald-400">${h.best_ms.toFixed(1)} ms</td>
|
|
|
|
|
+ <td class="p-3 text-rose-400">${h.wrst_ms.toFixed(1)} ms</td>
|
|
|
|
|
+ <td class="p-3 text-slate-400">${h.stdev_ms.toFixed(1)}</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tbody.innerHTML = `<tr><td colspan="9" class="p-4 text-center text-xs text-rose-400 font-mono">No MTR data collected</td></tr>`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const rawEl = document.getElementById('tools-mtr-raw');
|
|
|
|
|
+ if (rawEl) rawEl.innerText = data.raw_output || 'No output';
|
|
|
|
|
+
|
|
|
|
|
+ showToast('MTR Finished', `Report completed (${hopsCount} hops discovered)`, 'info');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('MTR Error', err.message, 'error');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (btn) btn.disabled = false;
|
|
|
|
|
+ if (spinner) spinner.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function executeDNS(e) {
|
|
|
|
|
+ if (e) e.preventDefault();
|
|
|
|
|
+ const domain = document.getElementById('tools-dns-domain')?.value?.trim();
|
|
|
|
|
+ const type = document.getElementById('tools-dns-type')?.value || 'A';
|
|
|
|
|
+ const server = document.getElementById('tools-dns-server')?.value?.trim() || '';
|
|
|
|
|
+
|
|
|
|
|
+ if (!domain) {
|
|
|
|
|
+ showToast('Input Required', 'Please enter a hostname or domain', 'warning');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const btn = document.getElementById('tools-dns-btn');
|
|
|
|
|
+ const spinner = document.getElementById('tools-dns-spinner');
|
|
|
|
|
+ if (btn) btn.disabled = true;
|
|
|
|
|
+ if (spinner) spinner.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/tools/dns`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ domain, type, server })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ showToast('DNS Error', data.error || 'Failed to resolve DNS query', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const resCard = document.getElementById('tools-dns-results');
|
|
|
|
|
+ if (resCard) resCard.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ const statTime = document.getElementById('tools-dns-stat-time');
|
|
|
|
|
+ if (statTime) statTime.innerText = `${data.query_time_ms ?? 0} ms`;
|
|
|
|
|
+
|
|
|
|
|
+ const statServer = document.getElementById('tools-dns-stat-server');
|
|
|
|
|
+ if (statServer) statServer.innerText = data.server || (server || 'System Resolver');
|
|
|
|
|
+
|
|
|
|
|
+ const tbody = document.getElementById('tools-dns-table-body');
|
|
|
|
|
+ if (tbody) {
|
|
|
|
|
+ if (Array.isArray(data.records) && data.records.length > 0) {
|
|
|
|
|
+ tbody.innerHTML = data.records.map(r => `
|
|
|
|
|
+ <tr class="hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs">
|
|
|
|
|
+ <td class="p-3 font-semibold text-white">${escapeHtml(r.name)}</td>
|
|
|
|
|
+ <td class="p-3 text-slate-400">${r.ttl}s</td>
|
|
|
|
|
+ <td class="p-3"><span class="px-2 py-0.5 rounded text-[10px] font-bold bg-cyan-950 text-cyan-300 border border-cyan-800/60">${escapeHtml(r.type)}</span></td>
|
|
|
|
|
+ <td class="p-3 font-bold text-amber-300 break-all">${escapeHtml(r.value)}</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `).join('');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tbody.innerHTML = `<tr><td colspan="4" class="p-4 text-center text-xs text-amber-400 font-mono">No answer records returned for ${escapeHtml(type)} query</td></tr>`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const rawEl = document.getElementById('tools-dns-raw');
|
|
|
|
|
+ if (rawEl) rawEl.innerText = data.raw_output || 'No output';
|
|
|
|
|
+
|
|
|
|
|
+ showToast('DNS Lookup Done', `Resolved ${domain} in ${data.query_time_ms} ms`, 'success');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('DNS Error', err.message, 'error');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (btn) btn.disabled = false;
|
|
|
|
|
+ if (spinner) spinner.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function executePortProbe(e) {
|
|
|
|
|
+ if (e) e.preventDefault();
|
|
|
|
|
+ const target = document.getElementById('tools-port-target')?.value?.trim();
|
|
|
|
|
+ const port = parseInt(document.getElementById('tools-port-num')?.value || '80', 10);
|
|
|
|
|
+ const timeout = parseInt(document.getElementById('tools-port-timeout')?.value || '3', 10);
|
|
|
|
|
+
|
|
|
|
|
+ if (!target || !port) {
|
|
|
|
|
+ showToast('Input Required', 'Please enter a target host and TCP port', 'warning');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const btn = document.getElementById('tools-port-btn');
|
|
|
|
|
+ const spinner = document.getElementById('tools-port-spinner');
|
|
|
|
|
+ if (btn) btn.disabled = true;
|
|
|
|
|
+ if (spinner) spinner.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/tools/port-probe`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ target, port, timeout })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ showToast('Probe Error', data.error || 'Failed to probe TCP port', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const resCard = document.getElementById('tools-port-results');
|
|
|
|
|
+ if (resCard) resCard.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ const banner = document.getElementById('tools-port-banner');
|
|
|
|
|
+ const icon = document.getElementById('tools-port-icon');
|
|
|
|
|
+ const title = document.getElementById('tools-port-title');
|
|
|
|
|
+ const sub = document.getElementById('tools-port-sub');
|
|
|
|
|
+ const latency = document.getElementById('tools-port-latency');
|
|
|
|
|
+
|
|
|
|
|
+ if (data.open) {
|
|
|
|
|
+ if (banner) banner.className = 'rounded-xl p-5 border bg-emerald-950/30 border-emerald-800/50 flex items-center justify-between';
|
|
|
|
|
+ if (icon) {
|
|
|
|
|
+ icon.className = 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm bg-emerald-500/20 text-emerald-400 border border-emerald-500/40';
|
|
|
|
|
+ icon.innerText = '✓';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (title) {
|
|
|
|
|
+ title.className = 'text-sm font-bold text-emerald-300';
|
|
|
|
|
+ title.innerText = `PORT ${port} OPEN / LISTENING`;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sub) {
|
|
|
|
|
+ sub.className = 'text-xs mt-0.5 font-mono text-emerald-400/80';
|
|
|
|
|
+ sub.innerText = `TCP 3-way handshake established with ${escapeHtml(target)}:${port}${data.service ? ' (' + escapeHtml(data.service) + ')' : ''}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (latency) {
|
|
|
|
|
+ latency.className = 'text-base font-bold font-mono text-emerald-300';
|
|
|
|
|
+ latency.innerText = `${data.latency_ms.toFixed(2)} ms`;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (banner) banner.className = 'rounded-xl p-5 border bg-rose-950/30 border-rose-800/50 flex items-center justify-between';
|
|
|
|
|
+ if (icon) {
|
|
|
|
|
+ icon.className = 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm bg-rose-500/20 text-rose-400 border border-rose-500/40';
|
|
|
|
|
+ icon.innerText = '✕';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (title) {
|
|
|
|
|
+ title.className = 'text-sm font-bold text-rose-300';
|
|
|
|
|
+ title.innerText = `PORT ${port} CLOSED / FILTERED`;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sub) {
|
|
|
|
|
+ sub.className = 'text-xs mt-0.5 font-mono text-rose-400/80';
|
|
|
|
|
+ sub.innerText = `Connection failed or timed out to ${escapeHtml(target)}:${port}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (latency) {
|
|
|
|
|
+ latency.className = 'text-base font-bold font-mono text-slate-500';
|
|
|
|
|
+ latency.innerText = '--';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const rawEl = document.getElementById('tools-port-raw');
|
|
|
|
|
+ if (rawEl) rawEl.innerText = data.raw_output || (data.open ? `Successfully connected to ${target}:${port} in ${data.latency_ms} ms` : `Connection failed: ${data.error || 'timed out or connection refused'}`);
|
|
|
|
|
+
|
|
|
|
|
+ showToast('Probe Complete', `Port ${port} is ${data.open ? 'OPEN' : 'CLOSED'}`, data.open ? 'success' : 'warning');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Probe Error', err.message, 'error');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (btn) btn.disabled = false;
|
|
|
|
|
+ if (spinner) spinner.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|