use anti::fs::{self, DuOptions, FormatUnit};
use anti::ping::{
resolve_hostname, PingConfig, PingError, PingMode, PingResult, PingStatistics, PingerBuilder,
TcpPinger,
};
use anti::sec::{hash_directory, hash_file, hash_reader, HashAlgorithm};
use anti::sys;
use anti::traceroute::{TraceProgress, Tracer, TracerouteConfig};
use anti::web_fetch;
use eyre::Result;
use base64;
use base64::Engine;
use bytesize::ByteSize;
use clap::{Args, Parser, Subcommand};
use console::style;
use humantime;
use serde_json;
use std::collections::HashMap;
use std::io::Write;
use std::process;
use std::time::Duration;
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Parser)]
#[command(name = "anti", version = VERSION, about = "Unified CLI toolbox")]
struct Cli {
#[command(subcommand)]
domain: Domain,
}
#[derive(Subcommand)]
enum Domain {
Net {
#[command(subcommand)]
command: NetCmd,
},
Sec {
#[command(subcommand)]
command: SecCmd,
},
Sys {
#[command(subcommand)]
command: SysCmd,
},
Web {
#[command(subcommand)]
command: WebCmd,
},
Fs {
#[command(subcommand)]
command: FsCmd,
},
}
#[derive(Subcommand)]
enum NetCmd {
Ping(PingOpts),
Trace(TraceOpts),
Scan(ScanOpts),
}
#[derive(Subcommand)]
enum SecCmd {
Hash(HashOpts),
}
#[derive(Subcommand)]
enum SysCmd {
Info,
}
#[derive(Subcommand)]
enum WebCmd {
Fetch(FetchOpts),
Load(LoadOpts),
}
#[derive(Args, Clone)]
struct FetchOpts {
url: String,
#[arg(long)]
head: bool,
#[arg(short = 'X', long, default_value = "GET")]
method: String,
#[arg(long)]
json: bool,
#[arg(short = 'v', long)]
verbose: bool,
#[arg(short = 'H', long = "header")]
headers: Vec<String>,
#[arg(short = 'd', long = "data")]
body: Option<String>,
#[arg(short = 'u', long = "user")]
auth: Option<String>,
#[arg(long)]
http09: bool,
#[arg(long)]
http10: bool,
#[arg(long)]
http11: bool,
#[arg(long)]
http2: bool,
#[arg(long, default_value_t = true)]
keep_alive: bool,
}
#[derive(Args, Clone)]
struct LoadOpts {
#[command(flatten)]
fetch: FetchOpts,
#[arg(short = 'n', long, default_value_t = 200)]
requests: u32,
#[arg(short = 'c', long, default_value_t = 50)]
connections: u32,
#[arg(short = 'p', long, default_value_t = 1)]
http2_parallel: u32,
#[arg(short = 'z', long)]
duration: Option<String>,
#[arg(short = 'w', long)]
wait: bool,
#[arg(short = 'q', long)]
qps: Option<u64>,
}
#[derive(Subcommand)]
enum FsCmd {
Du(DuOpts),
Df(DfOpts),
}
#[derive(Args)]
struct PingOpts {
host: String,
#[arg(long, default_value_t = 4)]
count: u16,
#[arg(long, default_value_t = 5)]
timeout: u64,
#[arg(long, value_enum, default_value = "icmp")]
protocol: Protocol,
#[arg(
long,
help = "Port for TCP/UDP ping (if not specified, TCP will probe common ports)"
)]
port: Option<u16>,
}
#[derive(Args)]
struct ScanOpts {
host: String,
#[arg(long)]
ports: Option<String>,
#[arg(long)]
port: Option<u16>,
#[arg(long)]
common: bool,
#[arg(long, value_enum, default_value = "tcp")]
protocol: ScanMode,
#[arg(long, help = "Disable progress spinner output")]
no_progress: bool,
#[arg(long, default_value_t = 0.3)]
timeout: f32,
#[arg(long, help = "Disable OS detection")]
no_os_detect: bool,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum ScanMode {
Tcp,
Udp,
Both,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum Protocol {
Icmp,
Udp,
Tcp,
}
#[derive(Args, Clone)]
struct TraceOpts {
host: String,
#[arg(long, value_enum, default_value = "udp")]
protocol: TraceProtocol,
#[arg(long, short = '6')]
ipv6: bool,
#[arg(long, short = 'm', default_value = "30")]
max_hops: u8,
#[arg(long, short = 'f', default_value = "1")]
first_hop: u8,
#[arg(long, short = 'W', default_value = "3")]
timeout: u64,
#[arg(long, short = 'p', default_value = "33434")]
port: u16,
#[arg(long, default_value = "80")]
tcp_port: u16,
#[arg(long, short = 'q', default_value = "3")]
attempts: u8,
#[arg(long, short = 's', default_value = "40")]
packet_size: usize,
#[arg(long, short = 'i', default_value = "0")]
interval: u64,
#[arg(long, short = 'S')]
source: Option<String>,
#[arg(long)]
interface: Option<String>,
#[arg(long, short = 'n')]
no_dns: bool,
#[arg(long)]
lookup_as: bool,
#[arg(long)]
lookup_geo: bool,
#[arg(long, value_enum, default_value = "text")]
format: OutputFormat,
#[arg(long, short = 'c')]
continuous: bool,
#[arg(long, short = 'r', default_value = "10")]
report_cycles: u32,
#[arg(long)]
report: bool,
#[arg(long)]
quiet: bool,
#[arg(long)]
summary: bool,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum TraceProtocol {
Udp,
Icmp,
Tcp,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum OutputFormat {
Text,
Json,
Csv,
Xml,
}
#[derive(Args, Clone)]
struct HashOpts {
files: Vec<String>,
#[arg(long, short = 'a', value_enum, default_value = "sha256")]
algorithm: HashAlgo,
#[arg(long, short = 'k')]
key: Option<String>,
#[arg(long)]
no_filename: bool,
#[arg(long, short = 'd', default_value_t = 5)]
max_depth: usize,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum HashAlgo {
Md5,
Sha1,
Sha256,
HmacMd5,
HmacSha1,
HmacSha256,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
enum SortKey {
Name,
Size,
#[value(name = "-name")]
NameReverse,
#[value(name = "-size")]
SizeReverse,
}
#[derive(Args)]
struct DuOpts {
#[arg(default_value = ".")]
paths: Vec<String>,
#[arg(long, default_value_t = 0)]
depth: usize,
#[arg(long, value_enum, default_value = "name")]
sort: SortKey,
#[arg(long, default_value = "human")]
unit: String,
}
#[derive(Args)]
struct DfOpts {
#[arg(default_value = ".")]
path: String,
#[arg(long, default_value = "human")]
unit: String,
#[arg(long, short)]
all: bool,
}
fn main() {
let cli = Cli::parse();
let result: eyre::Result<()> = match cli.domain {
Domain::Net { command: cmd } => match cmd {
NetCmd::Ping(opts) => net_ping(opts).map_err(|e| e.into()),
NetCmd::Trace(opts) => net_trace(opts).map_err(|e| e.into()),
NetCmd::Scan(opts) => net_scan_cmd(opts).map_err(|e| e.into()),
},
Domain::Fs { command: cmd } => match cmd {
FsCmd::Du(opts) => fs_du_cmd(opts),
FsCmd::Df(opts) => fs_df_cmd(opts),
},
Domain::Sec { command: cmd } => match cmd {
SecCmd::Hash(opts) => sec_hash(opts),
},
Domain::Sys { command: cmd } => match cmd {
SysCmd::Info => sys_info_cmd().map_err(|e| e.into()),
},
Domain::Web { command: cmd } => match cmd {
WebCmd::Fetch(opts) => web_fetch_cmd(opts).map_err(|e| e.into()),
WebCmd::Load(opts) => {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(web_load_cmd(opts)).map_err(|e| e.into())
}
},
};
if let Err(e) = result {
eprintln!("Error: {}", e);
process::exit(1);
}
}
fn net_ping(opts: PingOpts) -> PingResult<()> {
let target = resolve_hostname(&opts.host)?;
let config = PingConfig {
target,
count: opts.count,
timeout: Duration::from_secs(opts.timeout),
..Default::default()
};
let ping_mode = match opts.protocol {
Protocol::Icmp => PingMode::Icmp,
Protocol::Udp => PingMode::Udp,
Protocol::Tcp => PingMode::Tcp,
};
if matches!(opts.protocol, Protocol::Tcp) {
return handle_tcp_ping(opts, config);
}
let pinger = match opts.protocol {
Protocol::Icmp => PingerBuilder::new()
.target(config.target)
.count(config.count)
.timeout(config.timeout)
.interval(config.interval)
.packet_size(config.packet_size)
.build_icmp()?,
Protocol::Udp => {
if let Some(port) = opts.port {
PingerBuilder::new()
.target(config.target)
.count(config.count)
.timeout(config.timeout)
.interval(config.interval)
.packet_size(config.packet_size)
.build_udp_with_port(port)?
} else {
PingerBuilder::new()
.target(config.target)
.count(config.count)
.timeout(config.timeout)
.interval(config.interval)
.packet_size(config.packet_size)
.build_udp()?
}
}
Protocol::Tcp => unreachable!(), };
println!("PING {} ({}) using {}", opts.host, target, ping_mode);
let stats = pinger.ping_with_callback(|seq, result| match result {
Ok(reply) => println!(
"{} bytes from {}: seq={} time={:.2} ms",
reply.bytes_received,
reply.from,
reply.sequence,
reply.rtt.as_secs_f64() * 1000.0
),
Err(PingError::Timeout { .. }) => println!("Request timeout for seq={}", seq),
Err(e) => println!("Error for seq={}: {}", seq, e),
})?;
print_summary(&opts.host, &stats);
Ok(())
}
fn handle_tcp_ping(opts: PingOpts, config: PingConfig) -> PingResult<()> {
if let Some(port) = opts.port {
let pinger = PingerBuilder::new()
.target(config.target)
.count(config.count)
.timeout(config.timeout)
.interval(config.interval)
.packet_size(config.packet_size)
.build_tcp_with_port(port)?;
println!(
"PING {} ({}) using TCP on port {}",
opts.host, config.target, port
);
let stats = pinger.ping_with_callback(|seq, result| match result {
Ok(reply) => println!(
"Connected to {}:{}: seq={} time={:.2} ms",
reply.from,
port,
reply.sequence,
reply.rtt.as_secs_f64() * 1000.0
),
Err(PingError::Timeout { .. }) => println!("Request timeout for seq={}", seq),
Err(e) => println!("Error for seq={}: {}", seq, e),
})?;
print_summary(&opts.host, &stats);
} else {
println!(
"PING {} ({}) using TCP - probing common ports",
opts.host, config.target
);
let tcp_pinger = TcpPinger::new(config.clone());
if let Some((port, _reply)) = tcp_pinger.ping_first_open_port(1) {
println!(
"Found open port: {} ({})",
port,
anti::ping::tcp::classify_port(port)
);
let pinger = PingerBuilder::new()
.target(config.target)
.count(config.count)
.timeout(config.timeout)
.interval(config.interval)
.packet_size(config.packet_size)
.build_tcp_with_port(port)?;
println!("Using port {} for TCP ping test", port);
let stats = pinger.ping_with_callback(|seq, result| match result {
Ok(reply) => println!(
"Connected to {}:{}: seq={} time={:.2} ms",
reply.from,
port,
reply.sequence,
reply.rtt.as_secs_f64() * 1000.0
),
Err(PingError::Timeout { .. }) => println!("Request timeout for seq={}", seq),
Err(e) => println!("Error for seq={}: {}", seq, e),
})?;
print_summary(&opts.host, &stats);
} else {
println!("No open ports found on common ports. Tried: 80, 443, 22, 21, 25, 53, 110, 143, 993, 995, 3389, 5432, 3306");
return Err(PingError::Configuration {
message: "No open TCP ports found for ping test".to_string(),
});
}
}
Ok(())
}
fn print_summary(host: &str, stats: &PingStatistics) {
println!("\n--- {} ping statistics ---", host);
println!(
"{} packets transmitted, {} received, {:.1}% packet loss",
stats.packets_transmitted, stats.packets_received, stats.packet_loss
);
if stats.packets_received > 0 {
if let (Some(min), Some(avg), Some(max)) = (stats.min_rtt, stats.avg_rtt, stats.max_rtt) {
println!(
"round-trip min/avg/max = {:.2}/{:.2}/{:.2} ms",
min.as_secs_f64() * 1000.0,
avg.as_secs_f64() * 1000.0,
max.as_secs_f64() * 1000.0,
);
}
}
}
fn net_trace(opts: TraceOpts) -> PingResult<()> {
let source_address =
if let Some(ref src) = opts.source {
Some(src.parse().map_err(|_| {
PingError::InvalidTarget(format!("Invalid source address: {}", src))
})?)
} else {
None
};
let (target_v4, target_v6, ip_version) = if opts.ipv6 {
let target = resolve_hostname(&opts.host)?;
(
None,
Some(target.to_ipv6_mapped()),
anti::traceroute::IpVersion::V6,
)
} else {
let target = resolve_hostname(&opts.host)?;
(Some(target), None, anti::traceroute::IpVersion::V4)
};
let protocol = match opts.protocol {
TraceProtocol::Udp => anti::traceroute::TraceProtocol::Udp,
TraceProtocol::Icmp => anti::traceroute::TraceProtocol::Icmp,
TraceProtocol::Tcp => anti::traceroute::TraceProtocol::Tcp,
};
let output_format = match opts.format {
OutputFormat::Text => anti::traceroute::OutputFormat::Text,
OutputFormat::Json => anti::traceroute::OutputFormat::Json,
OutputFormat::Csv => anti::traceroute::OutputFormat::Csv,
OutputFormat::Xml => anti::traceroute::OutputFormat::Xml,
};
let mode = if opts.continuous {
anti::traceroute::TraceMode::Continuous
} else if opts.report {
anti::traceroute::TraceMode::Report
} else {
anti::traceroute::TraceMode::Single
};
let config = TracerouteConfig {
target_v4,
target_v6,
ip_version,
protocol,
max_hops: opts.max_hops,
first_hop: opts.first_hop,
timeout: Duration::from_secs(opts.timeout),
base_port: opts.port,
tcp_port: opts.tcp_port,
attempts: opts.attempts,
packet_size: opts.packet_size,
packet_interval: Duration::from_millis(opts.interval),
source_address,
source_interface: opts.interface.clone(),
resolve_hostnames: !opts.no_dns,
lookup_as: opts.lookup_as,
lookup_geo: opts.lookup_geo,
output_format,
mode,
report_cycles: opts.report_cycles,
quiet: opts.quiet,
summary_only: opts.summary,
..Default::default()
};
match mode {
anti::traceroute::TraceMode::Continuous => run_continuous_trace(opts.clone(), config),
anti::traceroute::TraceMode::Report => run_report_trace(opts.clone(), config),
anti::traceroute::TraceMode::Single => run_single_trace(opts.clone(), config),
}
}
fn run_single_trace(opts: TraceOpts, config: TracerouteConfig) -> PingResult<()> {
if !config.quiet {
let target = match config.ip_version {
anti::traceroute::IpVersion::V4 => {
config.target_v4.map(|a| a.to_string()).unwrap_or_default()
}
anti::traceroute::IpVersion::V6 => {
config.target_v6.map(|a| a.to_string()).unwrap_or_default()
}
};
println!(
"traceroute to {} ({}), {} hops max, {:?} protocol",
opts.host, target, config.max_hops, config.protocol
);
}
let tracer = Tracer::new(config.clone())?;
if matches!(config.output_format, anti::traceroute::OutputFormat::Text) && !config.quiet {
tracer.trace_with_progress(|progress| {
match progress {
TraceProgress::HopStart { ttl } => {
print!("{:>2} ", ttl);
std::io::stdout().flush().ok();
}
TraceProgress::Attempt { .. } => {
print!(".");
std::io::stdout().flush().ok();
}
TraceProgress::AttemptTimeout { .. } => {
print!("*");
std::io::stdout().flush().ok();
}
TraceProgress::FallbackStart { .. } => {
print!(" (icmp)");
std::io::stdout().flush().ok();
}
TraceProgress::HopComplete(hop) => {
print!("\r{:>2} ", hop.ttl);
if let Some(addr) = hop.addr() {
if let Some(hostname) = &hop.network_info.hostname {
print!("{} ({})", hostname, addr);
} else {
print!("{}", addr);
}
if let Some(rtt) = hop.rtt {
print!(" {:.3} ms", rtt.as_secs_f64() * 1000.0);
}
if config.lookup_as {
if let Some(as_num) = hop.network_info.as_number {
print!(" [AS{}]", as_num);
if let Some(as_name) = &hop.network_info.as_name {
print!(" {}", as_name);
}
}
}
if config.lookup_geo {
if let (Some(country), Some(city)) =
(&hop.network_info.country, &hop.network_info.city)
{
print!(" ({}, {})", city, country);
}
}
if hop.reached_destination {
println!(" <- destination reached");
} else {
println!();
}
} else {
println!("*");
}
}
}
true })?;
} else {
let hops = tracer.trace()?;
let output = tracer.format_output(&hops);
print!("{}", output);
}
Ok(())
}
fn run_continuous_trace(opts: TraceOpts, config: TracerouteConfig) -> PingResult<()> {
if !config.quiet {
let target = match config.ip_version {
anti::traceroute::IpVersion::V4 => {
config.target_v4.map(|a| a.to_string()).unwrap_or_default()
}
anti::traceroute::IpVersion::V6 => {
config.target_v6.map(|a| a.to_string()).unwrap_or_default()
}
};
println!(
"Continuous traceroute to {} ({}), Ctrl+C to stop",
opts.host, target
);
println!("TTL Address Loss% Snt Last Avg Best Wrst StDev");
}
let mut tracer = Tracer::new(config)?;
tracer.trace_continuous(|ttl, stats, network_info| {
if stats.sent > 0 {
let addr = network_info.hostname.as_deref().unwrap_or("???");
let loss = stats.loss_percent;
let sent = stats.sent;
let last = stats
.rtts
.last()
.map(|r| r.as_secs_f64() * 1000.0)
.unwrap_or(0.0);
let avg = stats
.avg_rtt
.map(|r| r.as_secs_f64() * 1000.0)
.unwrap_or(0.0);
let best = stats
.min_rtt
.map(|r| r.as_secs_f64() * 1000.0)
.unwrap_or(0.0);
let worst = stats
.max_rtt
.map(|r| r.as_secs_f64() * 1000.0)
.unwrap_or(0.0);
let stddev = stats.stddev_rtt.unwrap_or(0.0);
println!(
"{:>2} {:20} {:>5.1}% {:>5} {:>6.1} {:>5.1} {:>5.1} {:>5.1} {:>5.1}",
ttl, addr, loss, sent, last, avg, best, worst, stddev
);
}
true })?;
Ok(())
}
fn run_report_trace(opts: TraceOpts, config: TracerouteConfig) -> PingResult<()> {
if !config.quiet {
let target = match config.ip_version {
anti::traceroute::IpVersion::V4 => {
config.target_v4.map(|a| a.to_string()).unwrap_or_default()
}
anti::traceroute::IpVersion::V6 => {
config.target_v6.map(|a| a.to_string()).unwrap_or_default()
}
};
println!(
"Report mode: {} cycles to {} ({})",
config.report_cycles, opts.host, target
);
}
let mut tracer = Tracer::new(config.clone())?;
for cycle in 1..=config.report_cycles {
if !config.quiet {
print!("\rCycle {}/{}...", cycle, config.report_cycles);
std::io::stdout().flush().ok();
}
tracer.trace_continuous(|_ttl, _stats, _network_info| {
false })?;
}
if !config.quiet {
println!("\nReport complete. Final statistics:");
println!("TTL Address Loss% Snt Avg Best Wrst StDev Jitter");
}
println!("Report mode implementation needs completion...");
Ok(())
}
fn net_scan_cmd(opts: ScanOpts) -> PingResult<()> {
let proto = match opts.protocol {
ScanMode::Tcp => anti::scan::ScanProtocol::Tcp,
ScanMode::Udp => anti::scan::ScanProtocol::Udp,
ScanMode::Both => anti::scan::ScanProtocol::Both,
};
let ports = if let Some(p) = opts.port {
vec![p]
} else if let Some(range) = opts.ports.as_deref() {
anti::scan::parse_port_range(range)?
} else if opts.common {
anti::scan::common_ports_for(proto)
} else {
anti::scan::parse_port_range("1-1024")?
};
let rt = tokio::runtime::Runtime::new().unwrap();
let (_results, _os_map) = rt.block_on(anti::scan::scan_targets(
&opts.host,
&ports,
proto,
Duration::from_secs_f32(opts.timeout),
!opts.no_progress,
!opts.no_os_detect,
))?;
Ok(())
}
fn sec_hash(opts: HashOpts) -> eyre::Result<()> {
let algorithm = match opts.algorithm {
HashAlgo::Md5 => HashAlgorithm::Md5,
HashAlgo::Sha1 => HashAlgorithm::Sha1,
HashAlgo::Sha256 => HashAlgorithm::Sha256,
HashAlgo::HmacMd5 => HashAlgorithm::HmacMd5,
HashAlgo::HmacSha1 => HashAlgorithm::HmacSha1,
HashAlgo::HmacSha256 => HashAlgorithm::HmacSha256,
};
if opts.files.is_empty() {
let stdin = std::io::stdin();
let digest = hash_reader(
stdin.lock(),
algorithm,
opts.key.as_deref().map(|s| s.as_bytes()),
)?;
println!("{}", digest);
} else {
for file in opts.files {
if file == "-" {
let stdin = std::io::stdin();
let digest = hash_reader(
stdin.lock(),
algorithm,
opts.key.as_deref().map(|s| s.as_bytes()),
)?;
if opts.no_filename {
println!("{}", digest);
} else {
println!("{} -", digest);
}
continue;
}
let path = std::path::Path::new(&file);
let metadata = std::fs::metadata(path)?;
let digest = if metadata.is_dir() {
hash_directory(
path,
algorithm,
opts.key.as_deref().map(|s| s.as_bytes()),
opts.max_depth,
)?
} else {
hash_file(path, algorithm, opts.key.as_deref().map(|s| s.as_bytes()))?
};
if opts.no_filename {
println!("{}", digest);
} else {
println!("{} {}", digest, file);
}
}
}
Ok(())
}
fn fs_du_cmd(opts: DuOpts) -> Result<()> {
let unit: FormatUnit = opts.unit.parse()?;
let du_opts = DuOptions {
depth: opts.depth,
unit,
};
let paths = if opts.paths.is_empty() {
vec![".".to_string()]
} else {
opts.paths
};
for path in paths {
let mut results = fs::du(&path, &du_opts)?;
results.sort_by(|a, b| match opts.sort {
SortKey::Name => a.path.cmp(&b.path),
SortKey::Size => a.size.cmp(&b.size),
SortKey::NameReverse => b.path.cmp(&a.path),
SortKey::SizeReverse => b.size.cmp(&a.size),
});
for entry in results {
let size_str = fs::format_size(entry.size, unit);
println!("{}\t{}", size_str, entry.path.display());
}
}
Ok(())
}
fn sys_info_cmd() -> PingResult<()> {
let info = sys::gather();
let out = sys::format_plain(&info);
print!("{}", out);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_parses_sys_info() {
let cli = Cli::try_parse_from(["anti", "sys", "info"]).unwrap();
match cli.domain {
Domain::Sys {
command: SysCmd::Info,
} => {}
_ => panic!("Unexpected variant"),
}
}
}
fn web_fetch_cmd(opts: FetchOpts) -> PingResult<()> {
let mut method = opts.method.to_uppercase();
if opts.head {
method = "HEAD".into();
}
let mut headers_map = HashMap::new();
for h in opts.headers {
if let Some((k, v)) = h.split_once(':') {
headers_map.insert(k.trim().to_string(), v.trim().to_string());
}
}
if let Some(auth) = opts.auth {
let encoded = base64::engine::general_purpose::STANDARD.encode(auth);
headers_map.insert("Authorization".into(), format!("Basic {}", encoded));
}
let body = match opts.body {
Some(b) if b.starts_with('@') => {
let path = b.trim_start_matches('@');
match std::fs::read_to_string(path) {
Ok(content) => Some(content),
Err(e) => {
return Err(PingError::Configuration {
message: format!("failed to read body file: {}", e),
})
}
}
}
Some(b) => Some(b),
None => None,
};
let http_version = if opts.http09 {
Some(web_fetch::HttpVersion::Http09)
} else if opts.http10 {
Some(web_fetch::HttpVersion::Http10)
} else if opts.http11 {
Some(web_fetch::HttpVersion::Http11)
} else if opts.http2 {
Some(web_fetch::HttpVersion::Http2)
} else {
None
};
if opts.json || method == "HEAD" {
if opts.verbose {
eprintln!("> {} {}", method, opts.url);
for (k, v) in &headers_map {
eprintln!("> {}: {}", k, v);
}
}
match web_fetch::fetch(
&opts.url,
&method,
headers_map,
body,
http_version,
opts.keep_alive,
) {
Ok(resp) => {
if opts.verbose {
eprintln!("< Status: {}", resp.status);
eprintln!("< Version: {}", resp.version);
for (k, v) in &resp.headers {
eprintln!("< {}: {}", k, v);
}
}
if opts.json {
let json = web_fetch::to_json(&resp).map_err(|e| PingError::Configuration {
message: e.to_string(),
})?;
println!("{}", json);
} else {
println!("Status: {}", resp.status);
for (k, v) in resp.headers {
println!("{}: {}", k, v);
}
}
if method != "HEAD" {
if let Some(body) = resp.body {
print!("{}", body);
}
}
Ok(())
}
Err(e) => Err(PingError::Configuration {
message: e.to_string(),
}),
}
} else {
if opts.verbose {
eprintln!("> {} {}", method, opts.url);
for (k, v) in &headers_map {
eprintln!("> {}: {}", k, v);
}
}
match web_fetch::fetch_stream(
&opts.url,
&method,
headers_map,
body,
http_version,
opts.keep_alive,
) {
Ok(mut resp) => {
if opts.verbose {
eprintln!("< Status: {}", resp.status);
eprintln!("< Version: {}", resp.version);
for (k, v) in &resp.headers {
eprintln!("< {}: {}", k, v);
}
}
let mut handle = std::io::stdout().lock();
std::io::copy(&mut resp.response, &mut handle).map_err(|e| {
PingError::Configuration {
message: e.to_string(),
}
})?;
Ok(())
}
Err(e) => Err(PingError::Configuration {
message: e.to_string(),
}),
}
}
}
async fn web_load_cmd(opts: LoadOpts) -> PingResult<()> {
let duration = match opts.duration {
Some(ref d) => humantime::parse_duration(d).map_err(|e| PingError::Configuration {
message: e.to_string(),
})?,
None => std::time::Duration::from_secs(0),
};
let fetch_opts = opts.fetch;
let mut headers_map = HashMap::new();
for h in fetch_opts.headers {
if let Some((k, v)) = h.split_once(':') {
headers_map.insert(k.trim().to_string(), v.trim().to_string());
}
}
if let Some(auth) = fetch_opts.auth {
let encoded = base64::engine::general_purpose::STANDARD.encode(auth);
headers_map.insert("Authorization".into(), format!("Basic {}", encoded));
}
let body = match fetch_opts.body {
Some(b) if b.starts_with('@') => {
let path = b.trim_start_matches('@');
Some(
std::fs::read_to_string(path).map_err(|e| PingError::Configuration {
message: e.to_string(),
})?,
)
}
Some(b) => Some(b),
None => None,
};
let http_version = if fetch_opts.http09 {
Some(web_fetch::HttpVersion::Http09)
} else if fetch_opts.http10 {
Some(web_fetch::HttpVersion::Http10)
} else if fetch_opts.http11 {
Some(web_fetch::HttpVersion::Http11)
} else if fetch_opts.http2 {
Some(web_fetch::HttpVersion::Http2)
} else {
None
};
let load_opts = web_fetch::LoadOptions {
url: fetch_opts.url,
method: fetch_opts.method,
headers: headers_map,
body,
version: http_version,
keep_alive: fetch_opts.keep_alive,
requests: opts.requests,
connections: opts.connections as usize,
http2_parallel: opts.http2_parallel as usize,
duration: if duration.as_secs() == 0 {
None
} else {
Some(duration)
},
wait_after_deadline: opts.wait,
qps: opts.qps,
show_progress: !fetch_opts.json,
};
let result = web_fetch::load_test(load_opts)
.await
.map_err(|e| PingError::Configuration {
message: e.to_string(),
})?;
if fetch_opts.json {
let out = serde_json::to_string_pretty(&result).map_err(|e| PingError::Configuration {
message: e.to_string(),
})?;
println!("{}", out);
} else {
println!("\n{}", style("Summary").bold().underlined());
println!(" {:<16} {}", style("Total Requests:").cyan(), result.total);
println!(" {:<16} {}", style("Successes:").green(), result.successes);
println!(" {:<16} {}", style("Errors:").red(), result.errors);
println!(
" {:<16} {:.2}%",
style("Success Rate:").yellow(),
100.0 * result.successes as f64 / result.total as f64
);
println!(
" {:<16} {:.1}",
style("RPS:").magenta(),
result.successes as f64 / result.duration_secs
);
println!(
" {:<16} {:.2} ms",
style("Fastest:").cyan(),
result.fastest * 1000.0
);
println!(
" {:<16} {:.2} ms",
style("Average:").cyan(),
result.average * 1000.0
);
println!(
" {:<16} {:.2} ms",
style("Slowest:").cyan(),
result.slowest * 1000.0
);
println!(
" {:<16} {:.2} ms",
style("p95:").cyan(),
result.p95 * 1000.0
);
println!(
" {:<16} {:.2} ms",
style("p99:").cyan(),
result.p99 * 1000.0
);
let total_bytes = ByteSize::b(result.bytes);
println!(" {:<16} {}", style("Total Data:").cyan(), total_bytes);
if result.successes > 0 {
println!(
" {:<16} {}",
style("Size/Request:").cyan(),
ByteSize::b(result.bytes / result.successes as u64)
);
}
println!(
" {:<16} {}",
style("Size/sec:").cyan(),
ByteSize::b((result.bytes as f64 / result.duration_secs) as u64)
);
println!(
" {:<16} 1xx:{} 2xx:{} 3xx:{} 4xx:{} 5xx:{}",
style("Status:").cyan(),
result.status_counts[0],
result.status_counts[1],
result.status_counts[2],
result.status_counts[3],
result.status_counts[4]
);
}
Ok(())
}
fn fs_df_cmd(opts: DfOpts) -> Result<()> {
let unit: FormatUnit = opts.unit.parse()?;
if opts.all {
let infos = fs::df_all()?;
println!(
"{:<30} {:>12} {:>12} {:>12}",
"Filesystem", "Total", "Used", "Available"
);
for info in infos {
let total = fs::format_size(info.total, unit);
let used = fs::format_size(info.used, unit);
let avail = fs::format_size(info.available, unit);
println!(
"{:<30} {:>12} {:>12} {:>12}",
info.mount_point.display(),
total,
used,
avail
);
}
} else {
let info = fs::df(&opts.path)?;
let total = fs::format_size(info.total, unit);
let used = fs::format_size(info.used, unit);
let avail = fs::format_size(info.available, unit);
println!(
"{:<30} {:>12} {:>12} {:>12}",
"Filesystem", "Total", "Used", "Available"
);
println!(
"{:<30} {:>12} {:>12} {:>12}",
info.mount_point.display(),
total,
used,
avail
);
}
Ok(())
}