feat: Tools R2 — DNS, Whois, Bandwidth, Subnet Calculator
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 7s
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 7s
4 new tools with full backend + popup UIs: DNS Lookup: - dig/nslookup/host fallback chain on remote host - Record type selector (A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, PTR) Whois: - Remote whois query, first 80 lines - Works for domains and IP addresses Bandwidth Test (2 modes): - iperf3: LAN speed test between remote host and iperf server - Internet: speedtest-cli / curl-based Cloudflare test fallback Subnet Calculator: - Pure Rust, no SSH needed - CIDR input with quick-select buttons (/8 through /32) - Displays: network, broadcast, netmask, wildcard, host range, total/usable hosts, class, private/public Tools menu now has 11 items across 3 sections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
875dd1a28f
commit
b3f56a2729
@ -10,3 +10,4 @@ pub mod pty_commands;
|
|||||||
pub mod mcp_commands;
|
pub mod mcp_commands;
|
||||||
pub mod scanner_commands;
|
pub mod scanner_commands;
|
||||||
pub mod tools_commands;
|
pub mod tools_commands;
|
||||||
|
pub mod tools_commands_r2;
|
||||||
|
|||||||
201
src-tauri/src/commands/tools_commands_r2.rs
Normal file
201
src-tauri/src/commands/tools_commands_r2.rs
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
//! Tauri commands for Tools Round 2: DNS, Whois, Bandwidth, Subnet Calculator.
|
||||||
|
|
||||||
|
use tauri::State;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
// ── DNS Lookup ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn tool_dns_lookup(
|
||||||
|
session_id: String,
|
||||||
|
domain: String,
|
||||||
|
record_type: Option<String>,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let session = state.ssh.get_session(&session_id)
|
||||||
|
.ok_or_else(|| format!("SSH session {} not found", session_id))?;
|
||||||
|
let rtype = record_type.unwrap_or_else(|| "A".to_string());
|
||||||
|
let cmd = format!(
|
||||||
|
r#"dig {} {} +short 2>/dev/null || nslookup -type={} {} 2>/dev/null || host -t {} {} 2>/dev/null"#,
|
||||||
|
domain, rtype, rtype, domain, rtype, domain
|
||||||
|
);
|
||||||
|
exec_on_session(&session.handle, &cmd).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Whois ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn tool_whois(
|
||||||
|
session_id: String,
|
||||||
|
target: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let session = state.ssh.get_session(&session_id)
|
||||||
|
.ok_or_else(|| format!("SSH session {} not found", session_id))?;
|
||||||
|
let cmd = format!("whois {} 2>&1 | head -80", target);
|
||||||
|
exec_on_session(&session.handle, &cmd).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bandwidth Test ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn tool_bandwidth_iperf(
|
||||||
|
session_id: String,
|
||||||
|
server: String,
|
||||||
|
duration: Option<u32>,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let session = state.ssh.get_session(&session_id)
|
||||||
|
.ok_or_else(|| format!("SSH session {} not found", session_id))?;
|
||||||
|
let dur = duration.unwrap_or(5);
|
||||||
|
let cmd = format!(
|
||||||
|
"iperf3 -c {} -t {} --json 2>/dev/null || iperf3 -c {} -t {} 2>&1 || echo 'iperf3 not installed — run: apt install iperf3 / brew install iperf3'",
|
||||||
|
server, dur, server, dur
|
||||||
|
);
|
||||||
|
exec_on_session(&session.handle, &cmd).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn tool_bandwidth_speedtest(
|
||||||
|
session_id: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let session = state.ssh.get_session(&session_id)
|
||||||
|
.ok_or_else(|| format!("SSH session {} not found", session_id))?;
|
||||||
|
// Try multiple speedtest tools in order of preference
|
||||||
|
let cmd = r#"
|
||||||
|
if command -v speedtest-cli >/dev/null 2>&1; then
|
||||||
|
speedtest-cli --simple 2>&1
|
||||||
|
elif command -v speedtest >/dev/null 2>&1; then
|
||||||
|
speedtest --simple 2>&1
|
||||||
|
elif command -v curl >/dev/null 2>&1; then
|
||||||
|
echo "=== Download speed (curl) ==="
|
||||||
|
curl -o /dev/null -w "Download: %{speed_download} bytes/sec (%{size_download} bytes in %{time_total}s)\n" https://speed.cloudflare.com/__down?bytes=25000000 2>/dev/null
|
||||||
|
echo "=== Upload speed (curl) ==="
|
||||||
|
dd if=/dev/zero bs=1M count=10 2>/dev/null | curl -X POST -o /dev/null -w "Upload: %{speed_upload} bytes/sec (%{size_upload} bytes in %{time_total}s)\n" -d @- https://speed.cloudflare.com/__up 2>/dev/null
|
||||||
|
else
|
||||||
|
echo "No speedtest tool found. Install: pip install speedtest-cli"
|
||||||
|
fi
|
||||||
|
"#;
|
||||||
|
exec_on_session(&session.handle, cmd).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Subnet Calculator ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SubnetInfo {
|
||||||
|
pub cidr: String,
|
||||||
|
pub network: String,
|
||||||
|
pub broadcast: String,
|
||||||
|
pub netmask: String,
|
||||||
|
pub wildcard: String,
|
||||||
|
pub first_host: String,
|
||||||
|
pub last_host: String,
|
||||||
|
pub total_hosts: u64,
|
||||||
|
pub usable_hosts: u64,
|
||||||
|
pub prefix_length: u8,
|
||||||
|
pub class: String,
|
||||||
|
pub is_private: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure Rust subnet calculator — no SSH session needed.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn tool_subnet_calc(
|
||||||
|
cidr: String,
|
||||||
|
) -> Result<SubnetInfo, String> {
|
||||||
|
let parts: Vec<&str> = cidr.split('/').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err("Expected CIDR notation: e.g. 192.168.1.0/24".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ip_str = parts[0];
|
||||||
|
let prefix: u8 = parts[1].parse()
|
||||||
|
.map_err(|_| format!("Invalid prefix length: {}", parts[1]))?;
|
||||||
|
|
||||||
|
if prefix > 32 {
|
||||||
|
return Err(format!("Prefix length must be 0-32, got {}", prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
let octets: Vec<u8> = ip_str.split('.')
|
||||||
|
.map(|o| o.parse::<u8>())
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| format!("Invalid IP address: {}", ip_str))?;
|
||||||
|
|
||||||
|
if octets.len() != 4 {
|
||||||
|
return Err(format!("Invalid IP address: {}", ip_str));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ip: u32 = (octets[0] as u32) << 24
|
||||||
|
| (octets[1] as u32) << 16
|
||||||
|
| (octets[2] as u32) << 8
|
||||||
|
| (octets[3] as u32);
|
||||||
|
|
||||||
|
let mask: u32 = if prefix == 0 { 0 } else { !0u32 << (32 - prefix) };
|
||||||
|
let wildcard = !mask;
|
||||||
|
let network = ip & mask;
|
||||||
|
let broadcast = network | wildcard;
|
||||||
|
let first_host = if prefix >= 31 { network } else { network + 1 };
|
||||||
|
let last_host = if prefix >= 31 { broadcast } else { broadcast - 1 };
|
||||||
|
let total: u64 = 1u64 << (32 - prefix as u64);
|
||||||
|
let usable = if prefix >= 31 { total } else { total - 2 };
|
||||||
|
|
||||||
|
let class = match octets[0] {
|
||||||
|
0..=127 => "A",
|
||||||
|
128..=191 => "B",
|
||||||
|
192..=223 => "C",
|
||||||
|
224..=239 => "D (Multicast)",
|
||||||
|
_ => "E (Reserved)",
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_private = matches!(
|
||||||
|
(octets[0], octets[1]),
|
||||||
|
(10, _) | (172, 16..=31) | (192, 168)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(SubnetInfo {
|
||||||
|
cidr: format!("{}/{}", to_ip(network), prefix),
|
||||||
|
network: to_ip(network),
|
||||||
|
broadcast: to_ip(broadcast),
|
||||||
|
netmask: to_ip(mask),
|
||||||
|
wildcard: to_ip(wildcard),
|
||||||
|
first_host: to_ip(first_host),
|
||||||
|
last_host: to_ip(last_host),
|
||||||
|
total_hosts: total,
|
||||||
|
usable_hosts: usable,
|
||||||
|
prefix_length: prefix,
|
||||||
|
class: class.to_string(),
|
||||||
|
is_private,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_ip(val: u32) -> String {
|
||||||
|
format!("{}.{}.{}.{}", val >> 24, (val >> 16) & 0xFF, (val >> 8) & 0xFF, val & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helper ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async fn exec_on_session(
|
||||||
|
handle: &std::sync::Arc<tokio::sync::Mutex<russh::client::Handle<crate::ssh::session::SshClient>>>,
|
||||||
|
cmd: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let mut channel = {
|
||||||
|
let h = handle.lock().await;
|
||||||
|
h.channel_open_session().await.map_err(|e| format!("Exec channel failed: {}", e))?
|
||||||
|
};
|
||||||
|
channel.exec(true, cmd).await.map_err(|e| format!("Exec failed: {}", e))?;
|
||||||
|
let mut output = String::new();
|
||||||
|
loop {
|
||||||
|
match channel.wait().await {
|
||||||
|
Some(russh::ChannelMsg::Data { ref data }) => {
|
||||||
|
if let Ok(text) = std::str::from_utf8(data.as_ref()) { output.push_str(text); }
|
||||||
|
}
|
||||||
|
Some(russh::ChannelMsg::Eof) | Some(russh::ChannelMsg::Close) | None => break,
|
||||||
|
Some(russh::ChannelMsg::ExitStatus { .. }) => {}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
@ -142,6 +142,7 @@ pub fn run() {
|
|||||||
commands::mcp_commands::mcp_list_sessions, commands::mcp_commands::mcp_terminal_read, commands::mcp_commands::mcp_terminal_execute, commands::mcp_commands::mcp_get_session_context,
|
commands::mcp_commands::mcp_list_sessions, commands::mcp_commands::mcp_terminal_read, commands::mcp_commands::mcp_terminal_execute, commands::mcp_commands::mcp_get_session_context,
|
||||||
commands::scanner_commands::scan_network, commands::scanner_commands::scan_ports, commands::scanner_commands::quick_scan,
|
commands::scanner_commands::scan_network, commands::scanner_commands::scan_ports, commands::scanner_commands::quick_scan,
|
||||||
commands::tools_commands::tool_ping, commands::tools_commands::tool_traceroute, commands::tools_commands::tool_wake_on_lan, commands::tools_commands::tool_generate_ssh_key, commands::tools_commands::tool_generate_password,
|
commands::tools_commands::tool_ping, commands::tools_commands::tool_traceroute, commands::tools_commands::tool_wake_on_lan, commands::tools_commands::tool_generate_ssh_key, commands::tools_commands::tool_generate_password,
|
||||||
|
commands::tools_commands_r2::tool_dns_lookup, commands::tools_commands_r2::tool_whois, commands::tools_commands_r2::tool_bandwidth_iperf, commands::tools_commands_r2::tool_bandwidth_speedtest, commands::tools_commands_r2::tool_subnet_calc,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
44
src/components/tools/BandwidthTest.vue
Normal file
44
src/components/tools/BandwidthTest.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<select v-model="mode" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none cursor-pointer">
|
||||||
|
<option value="speedtest">Internet Speed Test</option>
|
||||||
|
<option value="iperf">iperf3 (LAN)</option>
|
||||||
|
</select>
|
||||||
|
<template v-if="mode === 'iperf'">
|
||||||
|
<input v-model="server" type="text" placeholder="iperf3 server IP" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-40" />
|
||||||
|
<input v-model.number="duration" type="number" min="1" max="60" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-16" />
|
||||||
|
<span class="text-xs text-[#484f58]">sec</span>
|
||||||
|
</template>
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="run">
|
||||||
|
{{ running ? "Testing..." : "Run Test" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Select a mode and click Run Test" }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
const props = defineProps<{ sessionId: string }>();
|
||||||
|
const mode = ref("speedtest");
|
||||||
|
const server = ref("");
|
||||||
|
const duration = ref(5);
|
||||||
|
const output = ref("");
|
||||||
|
const running = ref(false);
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
running.value = true;
|
||||||
|
output.value = mode.value === "iperf" ? `Running iperf3 to ${server.value}...\n` : "Running speed test...\n";
|
||||||
|
try {
|
||||||
|
if (mode.value === "iperf") {
|
||||||
|
if (!server.value) { output.value = "Enter an iperf3 server IP"; running.value = false; return; }
|
||||||
|
output.value = await invoke<string>("tool_bandwidth_iperf", { sessionId: props.sessionId, server: server.value, duration: duration.value });
|
||||||
|
} else {
|
||||||
|
output.value = await invoke<string>("tool_bandwidth_speedtest", { sessionId: props.sessionId });
|
||||||
|
}
|
||||||
|
} catch (err) { output.value = String(err); }
|
||||||
|
running.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
31
src/components/tools/DnsLookup.vue
Normal file
31
src/components/tools/DnsLookup.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="domain" type="text" placeholder="Domain name" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="lookup" />
|
||||||
|
<select v-model="recordType" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none cursor-pointer">
|
||||||
|
<option v-for="t in ['A','AAAA','MX','NS','TXT','CNAME','SOA','SRV','PTR']" :key="t" :value="t">{{ t }}</option>
|
||||||
|
</select>
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="lookup">Lookup</button>
|
||||||
|
</div>
|
||||||
|
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a domain and click Lookup" }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
const props = defineProps<{ sessionId: string }>();
|
||||||
|
const domain = ref("");
|
||||||
|
const recordType = ref("A");
|
||||||
|
const output = ref("");
|
||||||
|
const running = ref(false);
|
||||||
|
|
||||||
|
async function lookup(): Promise<void> {
|
||||||
|
if (!domain.value) return;
|
||||||
|
running.value = true;
|
||||||
|
try {
|
||||||
|
output.value = await invoke<string>("tool_dns_lookup", { sessionId: props.sessionId, domain: domain.value, recordType: recordType.value });
|
||||||
|
} catch (err) { output.value = String(err); }
|
||||||
|
running.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
49
src/components/tools/SubnetCalc.vue
Normal file
49
src/components/tools/SubnetCalc.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="cidr" type="text" placeholder="192.168.1.0/24" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-48 font-mono" @keydown.enter="calc" />
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer" @click="calc">Calculate</button>
|
||||||
|
<div class="flex items-center gap-1 ml-2">
|
||||||
|
<button v-for="quick in ['/8','/16','/24','/25','/26','/27','/28','/29','/30','/32']" :key="quick"
|
||||||
|
class="px-1.5 py-0.5 text-[10px] rounded bg-[#21262d] text-[#8b949e] hover:text-white hover:bg-[#30363d] cursor-pointer"
|
||||||
|
@click="cidr = cidr.replace(/\/\d+$/, '') + quick; calc()"
|
||||||
|
>{{ quick }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="info" class="grid grid-cols-2 gap-x-6 gap-y-2 text-xs">
|
||||||
|
<div><span class="text-[#8b949e]">CIDR:</span> <span class="font-mono">{{ info.cidr }}</span></div>
|
||||||
|
<div><span class="text-[#8b949e]">Class:</span> {{ info.class }} <span v-if="info.isPrivate" class="text-[#3fb950]">(Private)</span></div>
|
||||||
|
<div><span class="text-[#8b949e]">Network:</span> <span class="font-mono">{{ info.network }}</span></div>
|
||||||
|
<div><span class="text-[#8b949e]">Broadcast:</span> <span class="font-mono">{{ info.broadcast }}</span></div>
|
||||||
|
<div><span class="text-[#8b949e]">Netmask:</span> <span class="font-mono">{{ info.netmask }}</span></div>
|
||||||
|
<div><span class="text-[#8b949e]">Wildcard:</span> <span class="font-mono">{{ info.wildcard }}</span></div>
|
||||||
|
<div><span class="text-[#8b949e]">First Host:</span> <span class="font-mono">{{ info.firstHost }}</span></div>
|
||||||
|
<div><span class="text-[#8b949e]">Last Host:</span> <span class="font-mono">{{ info.lastHost }}</span></div>
|
||||||
|
<div><span class="text-[#8b949e]">Total Hosts:</span> {{ info.totalHosts.toLocaleString() }}</div>
|
||||||
|
<div><span class="text-[#8b949e]">Usable Hosts:</span> {{ info.usableHosts.toLocaleString() }}</div>
|
||||||
|
<div><span class="text-[#8b949e]">Prefix Length:</span> /{{ info.prefixLength }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
const cidr = ref("192.168.1.0/24");
|
||||||
|
|
||||||
|
interface SubnetInfo {
|
||||||
|
cidr: string; network: string; broadcast: string; netmask: string; wildcard: string;
|
||||||
|
firstHost: string; lastHost: string; totalHosts: number; usableHosts: number;
|
||||||
|
prefixLength: number; class: string; isPrivate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = ref<SubnetInfo | null>(null);
|
||||||
|
|
||||||
|
async function calc(): Promise<void> {
|
||||||
|
if (!cidr.value) return;
|
||||||
|
try { info.value = await invoke<SubnetInfo>("tool_subnet_calc", { cidr: cidr.value }); }
|
||||||
|
catch (err) { alert(err); }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -5,6 +5,10 @@
|
|||||||
<PingTool v-else-if="tool === 'ping'" :session-id="sessionId" />
|
<PingTool v-else-if="tool === 'ping'" :session-id="sessionId" />
|
||||||
<TracerouteTool v-else-if="tool === 'traceroute'" :session-id="sessionId" />
|
<TracerouteTool v-else-if="tool === 'traceroute'" :session-id="sessionId" />
|
||||||
<WakeOnLan v-else-if="tool === 'wake-on-lan'" :session-id="sessionId" />
|
<WakeOnLan v-else-if="tool === 'wake-on-lan'" :session-id="sessionId" />
|
||||||
|
<DnsLookup v-else-if="tool === 'dns-lookup'" :session-id="sessionId" />
|
||||||
|
<WhoisTool v-else-if="tool === 'whois'" :session-id="sessionId" />
|
||||||
|
<BandwidthTest v-else-if="tool === 'bandwidth'" :session-id="sessionId" />
|
||||||
|
<SubnetCalc v-else-if="tool === 'subnet-calc'" />
|
||||||
<SshKeyGen v-else-if="tool === 'ssh-keygen'" />
|
<SshKeyGen v-else-if="tool === 'ssh-keygen'" />
|
||||||
<PasswordGen v-else-if="tool === 'password-gen'" />
|
<PasswordGen v-else-if="tool === 'password-gen'" />
|
||||||
<div v-else class="flex-1 flex items-center justify-center text-sm text-[#484f58]">
|
<div v-else class="flex-1 flex items-center justify-center text-sm text-[#484f58]">
|
||||||
@ -19,6 +23,10 @@ import PortScanner from "./PortScanner.vue";
|
|||||||
import PingTool from "./PingTool.vue";
|
import PingTool from "./PingTool.vue";
|
||||||
import TracerouteTool from "./TracerouteTool.vue";
|
import TracerouteTool from "./TracerouteTool.vue";
|
||||||
import WakeOnLan from "./WakeOnLan.vue";
|
import WakeOnLan from "./WakeOnLan.vue";
|
||||||
|
import DnsLookup from "./DnsLookup.vue";
|
||||||
|
import WhoisTool from "./WhoisTool.vue";
|
||||||
|
import BandwidthTest from "./BandwidthTest.vue";
|
||||||
|
import SubnetCalc from "./SubnetCalc.vue";
|
||||||
import SshKeyGen from "./SshKeyGen.vue";
|
import SshKeyGen from "./SshKeyGen.vue";
|
||||||
import PasswordGen from "./PasswordGen.vue";
|
import PasswordGen from "./PasswordGen.vue";
|
||||||
|
|
||||||
|
|||||||
26
src/components/tools/WhoisTool.vue
Normal file
26
src/components/tools/WhoisTool.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="target" type="text" placeholder="Domain or IP" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="lookup" />
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="lookup">Whois</button>
|
||||||
|
</div>
|
||||||
|
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a domain or IP and click Whois" }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
const props = defineProps<{ sessionId: string }>();
|
||||||
|
const target = ref("");
|
||||||
|
const output = ref("");
|
||||||
|
const running = ref(false);
|
||||||
|
|
||||||
|
async function lookup(): Promise<void> {
|
||||||
|
if (!target.value) return;
|
||||||
|
running.value = true;
|
||||||
|
try { output.value = await invoke<string>("tool_whois", { sessionId: props.sessionId, target: target.value }); }
|
||||||
|
catch (err) { output.value = String(err); }
|
||||||
|
running.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -88,6 +88,30 @@
|
|||||||
>
|
>
|
||||||
<span class="flex-1">Traceroute</span>
|
<span class="flex-1">Traceroute</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||||
|
@mousedown.prevent="handleToolAction('dns-lookup')"
|
||||||
|
>
|
||||||
|
<span class="flex-1">DNS Lookup</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||||
|
@mousedown.prevent="handleToolAction('whois')"
|
||||||
|
>
|
||||||
|
<span class="flex-1">Whois</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||||
|
@mousedown.prevent="handleToolAction('bandwidth')"
|
||||||
|
>
|
||||||
|
<span class="flex-1">Bandwidth Test</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||||
|
@mousedown.prevent="handleToolAction('subnet-calc')"
|
||||||
|
>
|
||||||
|
<span class="flex-1">Subnet Calculator</span>
|
||||||
|
</button>
|
||||||
<div class="border-t border-[#30363d] my-1" />
|
<div class="border-t border-[#30363d] my-1" />
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||||
@ -304,7 +328,7 @@ async function handleToolAction(tool: string): Promise<void> {
|
|||||||
showToolsMenu.value = false;
|
showToolsMenu.value = false;
|
||||||
|
|
||||||
// Tools that don't need a session
|
// Tools that don't need a session
|
||||||
const localTools = ["ssh-keygen", "password-gen"];
|
const localTools = ["ssh-keygen", "password-gen", "subnet-calc"];
|
||||||
|
|
||||||
if (!localTools.includes(tool) && !activeSessionId.value) {
|
if (!localTools.includes(tool) && !activeSessionId.value) {
|
||||||
alert("Connect to a server first — network tools run through SSH sessions.");
|
alert("Connect to a server first — network tools run through SSH sessions.");
|
||||||
@ -318,6 +342,10 @@ async function handleToolAction(tool: string): Promise<void> {
|
|||||||
"port-scanner": { title: "Port Scanner", width: 700, height: 500 },
|
"port-scanner": { title: "Port Scanner", width: 700, height: 500 },
|
||||||
"ping": { title: "Ping", width: 600, height: 400 },
|
"ping": { title: "Ping", width: 600, height: 400 },
|
||||||
"traceroute": { title: "Traceroute", width: 600, height: 500 },
|
"traceroute": { title: "Traceroute", width: 600, height: 500 },
|
||||||
|
"dns-lookup": { title: "DNS Lookup", width: 600, height: 400 },
|
||||||
|
"whois": { title: "Whois", width: 700, height: 500 },
|
||||||
|
"bandwidth": { title: "Bandwidth Test", width: 700, height: 450 },
|
||||||
|
"subnet-calc": { title: "Subnet Calculator", width: 650, height: 350 },
|
||||||
"wake-on-lan": { title: "Wake on LAN", width: 500, height: 300 },
|
"wake-on-lan": { title: "Wake on LAN", width: 500, height: 300 },
|
||||||
"ssh-keygen": { title: "SSH Key Generator", width: 700, height: 500 },
|
"ssh-keygen": { title: "SSH Key Generator", width: 700, height: 500 },
|
||||||
"password-gen": { title: "Password Generator", width: 500, height: 400 },
|
"password-gen": { title: "Password Generator", width: 500, height: 400 },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user