Add multiple features

This commit is contained in:
DaZuo0122
2026-01-16 23:16:58 +08:00
parent c367ca29e4
commit cb022127c0
18 changed files with 1883 additions and 4 deletions

View File

@@ -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;