Add multiple features
This commit is contained in:
@@ -18,6 +18,10 @@ wtfnet-geoip = { path = "../wtfnet-geoip" }
|
||||
wtfnet-platform = { path = "../wtfnet-platform" }
|
||||
wtfnet-probe = { path = "../wtfnet-probe" }
|
||||
wtfnet-dns = { path = "../wtfnet-dns", features = ["pcap"] }
|
||||
wtfnet-http = { path = "../wtfnet-http" }
|
||||
wtfnet-tls = { path = "../wtfnet-tls" }
|
||||
wtfnet-discover = { path = "../wtfnet-discover" }
|
||||
wtfnet-diag = { path = "../wtfnet-diag" }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
wtfnet-platform-windows = { path = "../wtfnet-platform-windows" }
|
||||
|
||||
@@ -70,6 +70,19 @@ enum Commands {
|
||||
#[command(subcommand)]
|
||||
command: CalcCommand,
|
||||
},
|
||||
Http {
|
||||
#[command(subcommand)]
|
||||
command: HttpCommand,
|
||||
},
|
||||
Tls {
|
||||
#[command(subcommand)]
|
||||
command: TlsCommand,
|
||||
},
|
||||
Discover {
|
||||
#[command(subcommand)]
|
||||
command: DiscoverCommand,
|
||||
},
|
||||
Diag(DiagArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
@@ -124,6 +137,26 @@ enum CalcCommand {
|
||||
Summarize(CalcSummarizeArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum HttpCommand {
|
||||
Head(HttpRequestArgs),
|
||||
Get(HttpRequestArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum TlsCommand {
|
||||
Handshake(TlsArgs),
|
||||
Cert(TlsArgs),
|
||||
Verify(TlsArgs),
|
||||
Alpn(TlsArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum DiscoverCommand {
|
||||
Mdns(DiscoverMdnsArgs),
|
||||
Ssdp(DiscoverSsdpArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct SysIpArgs {
|
||||
#[arg(long)]
|
||||
@@ -276,6 +309,68 @@ struct CalcSummarizeArgs {
|
||||
cidrs: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct HttpRequestArgs {
|
||||
url: String,
|
||||
#[arg(long, default_value_t = 3000)]
|
||||
timeout_ms: u64,
|
||||
#[arg(long)]
|
||||
follow_redirects: Option<u32>,
|
||||
#[arg(long)]
|
||||
show_headers: bool,
|
||||
#[arg(long)]
|
||||
show_body: bool,
|
||||
#[arg(long, default_value_t = 8192)]
|
||||
max_body_bytes: usize,
|
||||
#[arg(long)]
|
||||
http1_only: bool,
|
||||
#[arg(long)]
|
||||
http2_only: bool,
|
||||
#[arg(long)]
|
||||
geoip: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct TlsArgs {
|
||||
target: String,
|
||||
#[arg(long)]
|
||||
sni: Option<String>,
|
||||
#[arg(long)]
|
||||
alpn: Option<String>,
|
||||
#[arg(long, default_value_t = 3000)]
|
||||
timeout_ms: u64,
|
||||
#[arg(long)]
|
||||
insecure: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct DiscoverMdnsArgs {
|
||||
#[arg(long, default_value = "3s")]
|
||||
duration: String,
|
||||
#[arg(long)]
|
||||
service: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct DiscoverSsdpArgs {
|
||||
#[arg(long, default_value = "3s")]
|
||||
duration: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct DiagArgs {
|
||||
#[arg(long)]
|
||||
out: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
bundle: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
dns_detect: Option<String>,
|
||||
#[arg(long, default_value_t = 2000)]
|
||||
dns_timeout_ms: u64,
|
||||
#[arg(long, default_value_t = 3)]
|
||||
dns_repeat: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct DnsAnswerGeoIp {
|
||||
pub name: String,
|
||||
@@ -326,6 +421,20 @@ struct CalcSummarizeReport {
|
||||
pub result: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct HttpReportGeoIp {
|
||||
pub url: String,
|
||||
pub final_url: Option<String>,
|
||||
pub method: String,
|
||||
pub status: Option<u16>,
|
||||
pub http_version: Option<String>,
|
||||
pub resolved_ips: Vec<String>,
|
||||
pub geoip: Vec<wtfnet_geoip::GeoIpRecord>,
|
||||
pub headers: Vec<(String, String)>,
|
||||
pub body: Option<String>,
|
||||
pub timing: wtfnet_http::HttpTiming,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
@@ -396,6 +505,31 @@ async fn main() {
|
||||
Commands::Calc {
|
||||
command: CalcCommand::Summarize(args),
|
||||
} => handle_calc_summarize(&cli, args.clone()).await,
|
||||
Commands::Http {
|
||||
command: HttpCommand::Head(args),
|
||||
} => handle_http_request(&cli, args.clone(), wtfnet_http::HttpMethod::Head).await,
|
||||
Commands::Http {
|
||||
command: HttpCommand::Get(args),
|
||||
} => handle_http_request(&cli, args.clone(), wtfnet_http::HttpMethod::Get).await,
|
||||
Commands::Tls {
|
||||
command: TlsCommand::Handshake(args),
|
||||
} => handle_tls_handshake(&cli, args.clone()).await,
|
||||
Commands::Tls {
|
||||
command: TlsCommand::Cert(args),
|
||||
} => handle_tls_cert(&cli, args.clone()).await,
|
||||
Commands::Tls {
|
||||
command: TlsCommand::Verify(args),
|
||||
} => handle_tls_verify(&cli, args.clone()).await,
|
||||
Commands::Tls {
|
||||
command: TlsCommand::Alpn(args),
|
||||
} => handle_tls_alpn(&cli, args.clone()).await,
|
||||
Commands::Discover {
|
||||
command: DiscoverCommand::Mdns(args),
|
||||
} => handle_discover_mdns(&cli, args.clone()).await,
|
||||
Commands::Discover {
|
||||
command: DiscoverCommand::Ssdp(args),
|
||||
} => handle_discover_ssdp(&cli, args.clone()).await,
|
||||
Commands::Diag(args) => handle_diag(&cli, args.clone()).await,
|
||||
};
|
||||
|
||||
std::process::exit(exit_code);
|
||||
@@ -1553,6 +1687,378 @@ fn split_host_port_with_default(value: &str, default_port: u16) -> Result<(Strin
|
||||
Ok((value.to_string(), default_port))
|
||||
}
|
||||
|
||||
async fn handle_http_request(
|
||||
cli: &Cli,
|
||||
args: HttpRequestArgs,
|
||||
method: wtfnet_http::HttpMethod,
|
||||
) -> i32 {
|
||||
let opts = wtfnet_http::HttpRequestOptions {
|
||||
method,
|
||||
timeout_ms: args.timeout_ms,
|
||||
follow_redirects: args.follow_redirects,
|
||||
max_body_bytes: args.max_body_bytes,
|
||||
show_headers: args.show_headers,
|
||||
show_body: args.show_body,
|
||||
http1_only: args.http1_only,
|
||||
http2_only: args.http2_only,
|
||||
};
|
||||
|
||||
match wtfnet_http::request(&args.url, opts).await {
|
||||
Ok(report) => {
|
||||
let enriched = if args.geoip {
|
||||
let service = geoip_service();
|
||||
let geoip = report
|
||||
.resolved_ips
|
||||
.iter()
|
||||
.filter_map(|value| value.parse::<std::net::IpAddr>().ok())
|
||||
.map(|ip| service.lookup(ip))
|
||||
.collect::<Vec<_>>();
|
||||
HttpReportGeoIp {
|
||||
url: report.url.clone(),
|
||||
final_url: report.final_url.clone(),
|
||||
method: report.method.clone(),
|
||||
status: report.status,
|
||||
http_version: report.http_version.clone(),
|
||||
resolved_ips: report.resolved_ips.clone(),
|
||||
geoip,
|
||||
headers: report.headers.clone(),
|
||||
body: report.body.clone(),
|
||||
timing: report.timing.clone(),
|
||||
}
|
||||
} else {
|
||||
HttpReportGeoIp {
|
||||
url: report.url.clone(),
|
||||
final_url: report.final_url.clone(),
|
||||
method: report.method.clone(),
|
||||
status: report.status,
|
||||
http_version: report.http_version.clone(),
|
||||
resolved_ips: report.resolved_ips.clone(),
|
||||
geoip: Vec::new(),
|
||||
headers: report.headers.clone(),
|
||||
body: report.body.clone(),
|
||||
timing: report.timing.clone(),
|
||||
}
|
||||
};
|
||||
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new("http request", vec![args.url]);
|
||||
let envelope = CommandEnvelope::new(meta, command, enriched);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
println!(
|
||||
"status: {}",
|
||||
report
|
||||
.status
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "-".to_string())
|
||||
);
|
||||
if let Some(url) = report.final_url.as_ref() {
|
||||
println!("final_url: {url}");
|
||||
}
|
||||
if let Some(version) = report.http_version.as_ref() {
|
||||
println!("version: {version}");
|
||||
}
|
||||
if !report.resolved_ips.is_empty() {
|
||||
println!("resolved: {}", report.resolved_ips.join(", "));
|
||||
}
|
||||
println!("total_ms: {}", report.timing.total_ms);
|
||||
if let Some(ms) = report.timing.dns_ms {
|
||||
println!("dns_ms: {ms}");
|
||||
}
|
||||
if let Some(ms) = report.timing.connect_ms {
|
||||
println!("connect_ms: {ms}");
|
||||
}
|
||||
if let Some(ms) = report.timing.tls_ms {
|
||||
println!("tls_ms: {ms}");
|
||||
}
|
||||
if let Some(ms) = report.timing.ttfb_ms {
|
||||
println!("ttfb_ms: {ms}");
|
||||
}
|
||||
if args.geoip && !enriched.geoip.is_empty() {
|
||||
for entry in &enriched.geoip {
|
||||
println!("geoip {}: {}", entry.ip, format_geoip(entry));
|
||||
}
|
||||
}
|
||||
if !report.headers.is_empty() {
|
||||
println!("headers:");
|
||||
for (name, value) in report.headers {
|
||||
println!(" {name}: {value}");
|
||||
}
|
||||
}
|
||||
if let Some(body) = report.body.as_ref() {
|
||||
println!("body:");
|
||||
println!("{body}");
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("http request failed: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_tls_handshake(cli: &Cli, args: TlsArgs) -> i32 {
|
||||
let options = build_tls_options(&args);
|
||||
match wtfnet_tls::handshake(&args.target, options).await {
|
||||
Ok(report) => emit_tls_report(cli, "tls handshake", report),
|
||||
Err(err) => {
|
||||
eprintln!("tls handshake failed: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_tls_cert(cli: &Cli, args: TlsArgs) -> i32 {
|
||||
let options = build_tls_options(&args);
|
||||
match wtfnet_tls::certs(&args.target, options).await {
|
||||
Ok(report) => emit_tls_report(cli, "tls cert", report),
|
||||
Err(err) => {
|
||||
eprintln!("tls cert failed: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_tls_verify(cli: &Cli, args: TlsArgs) -> i32 {
|
||||
let options = build_tls_options(&args);
|
||||
match wtfnet_tls::verify(&args.target, options).await {
|
||||
Ok(report) => emit_tls_report(cli, "tls verify", report),
|
||||
Err(err) => {
|
||||
eprintln!("tls verify failed: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_tls_alpn(cli: &Cli, args: TlsArgs) -> i32 {
|
||||
let options = build_tls_options(&args);
|
||||
match wtfnet_tls::alpn(&args.target, options).await {
|
||||
Ok(report) => emit_tls_report(cli, "tls alpn", report),
|
||||
Err(err) => {
|
||||
eprintln!("tls alpn failed: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tls_options(args: &TlsArgs) -> wtfnet_tls::TlsOptions {
|
||||
wtfnet_tls::TlsOptions {
|
||||
sni: args.sni.clone(),
|
||||
alpn: parse_alpn(args.alpn.as_deref()),
|
||||
timeout_ms: args.timeout_ms,
|
||||
insecure: args.insecure,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_alpn(value: Option<&str>) -> Vec<String> {
|
||||
let Some(value) = value else { return Vec::new() };
|
||||
value
|
||||
.split(',')
|
||||
.map(|part| part.trim())
|
||||
.filter(|part| !part.is_empty())
|
||||
.map(|part| part.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn emit_tls_report<T: serde::Serialize>(cli: &Cli, name: &str, report: T) -> i32 {
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new(name, Vec::new());
|
||||
let envelope = CommandEnvelope::new(meta, command, report);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
let value = serde_json::to_value(report).unwrap_or(serde_json::Value::Null);
|
||||
println!("{}", serde_json::to_string_pretty(&value).unwrap_or_default());
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_discover_mdns(cli: &Cli, args: DiscoverMdnsArgs) -> i32 {
|
||||
let duration_ms = match parse_duration_ms(&args.duration) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
return ExitKind::Usage.code();
|
||||
}
|
||||
};
|
||||
let options = wtfnet_discover::MdnsOptions {
|
||||
duration_ms,
|
||||
service_type: args.service.clone(),
|
||||
};
|
||||
match wtfnet_discover::mdns_discover(options).await {
|
||||
Ok(report) => {
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let mut command_args = vec!["--duration".to_string(), args.duration];
|
||||
if let Some(service) = args.service {
|
||||
command_args.push("--service".to_string());
|
||||
command_args.push(service);
|
||||
}
|
||||
let command = CommandInfo::new("discover mdns", command_args);
|
||||
let envelope = CommandEnvelope::new(meta, command, report);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
for service in report.services {
|
||||
println!("{}", service.fullname);
|
||||
println!(" type: {}", service.service_type);
|
||||
if let Some(hostname) = service.hostname {
|
||||
println!(" host: {hostname}");
|
||||
}
|
||||
if !service.addresses.is_empty() {
|
||||
println!(" addrs: {}", service.addresses.join(", "));
|
||||
}
|
||||
if let Some(port) = service.port {
|
||||
println!(" port: {port}");
|
||||
}
|
||||
if !service.properties.is_empty() {
|
||||
println!(" props:");
|
||||
for (key, value) in service.properties {
|
||||
println!(" {key}={value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("mdns discover failed: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_discover_ssdp(cli: &Cli, args: DiscoverSsdpArgs) -> i32 {
|
||||
let duration_ms = match parse_duration_ms(&args.duration) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
return ExitKind::Usage.code();
|
||||
}
|
||||
};
|
||||
let options = wtfnet_discover::SsdpOptions { duration_ms };
|
||||
match wtfnet_discover::ssdp_discover(options).await {
|
||||
Ok(report) => {
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new(
|
||||
"discover ssdp",
|
||||
vec!["--duration".to_string(), args.duration],
|
||||
);
|
||||
let envelope = CommandEnvelope::new(meta, command, report);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
for service in report.services {
|
||||
println!("from: {}", service.from);
|
||||
if let Some(st) = service.st {
|
||||
println!(" st: {st}");
|
||||
}
|
||||
if let Some(usn) = service.usn {
|
||||
println!(" usn: {usn}");
|
||||
}
|
||||
if let Some(location) = service.location {
|
||||
println!(" location: {location}");
|
||||
}
|
||||
if let Some(server) = service.server {
|
||||
println!(" server: {server}");
|
||||
}
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("ssdp discover failed: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_diag(cli: &Cli, args: DiagArgs) -> i32 {
|
||||
let options = wtfnet_diag::DiagOptions {
|
||||
dns_detect_domain: args.dns_detect.clone(),
|
||||
dns_detect_timeout_ms: args.dns_timeout_ms,
|
||||
dns_detect_repeat: args.dns_repeat,
|
||||
};
|
||||
let platform = platform();
|
||||
let report = match wtfnet_diag::run(&platform, options).await {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
eprintln!("diag failed: {err}");
|
||||
return ExitKind::Failed.code();
|
||||
}
|
||||
};
|
||||
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new("diag", Vec::new());
|
||||
let envelope = CommandEnvelope::new(meta.clone(), command, report.clone());
|
||||
let json = if cli.pretty {
|
||||
serde_json::to_string_pretty(&envelope)
|
||||
} else {
|
||||
serde_json::to_string(&envelope)
|
||||
};
|
||||
|
||||
if let Some(out) = args.out.as_ref() {
|
||||
if let Ok(payload) = json.as_ref() {
|
||||
if let Err(err) = std::fs::write(out, payload) {
|
||||
eprintln!("failed to write diag output: {err}");
|
||||
return ExitKind::Failed.code();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(bundle) = args.bundle.as_ref() {
|
||||
let meta_json =
|
||||
serde_json::to_value(&meta).unwrap_or_else(|_| serde_json::Value::Null);
|
||||
let report_json =
|
||||
serde_json::to_value(&report).unwrap_or_else(|_| serde_json::Value::Null);
|
||||
if let Err(err) = wtfnet_diag::write_bundle(bundle, &meta_json, &report_json) {
|
||||
eprintln!("failed to write bundle: {err}");
|
||||
return ExitKind::Failed.code();
|
||||
}
|
||||
}
|
||||
|
||||
if cli.json {
|
||||
match json {
|
||||
Ok(payload) => {
|
||||
println!("{payload}");
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed to serialize json: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"ifaces: {} routes: {} ports: {} neigh: {} warnings: {}",
|
||||
report
|
||||
.interfaces
|
||||
.as_ref()
|
||||
.map(|value| value.len().to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
report
|
||||
.routes
|
||||
.as_ref()
|
||||
.map(|value| value.len().to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
report
|
||||
.ports_listen
|
||||
.as_ref()
|
||||
.map(|value| value.len().to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
report
|
||||
.neighbors
|
||||
.as_ref()
|
||||
.map(|value| value.len().to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
report.warnings.len()
|
||||
);
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_tls_name(value: &str) -> Option<String> {
|
||||
if let Ok(_addr) = value.parse::<std::net::SocketAddr>() {
|
||||
return None;
|
||||
|
||||
Reference in New Issue
Block a user