use anyhow::Result;
use ipnet::IpNet;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum IpRpkiValidationState {
#[serde(rename = "valid")]
Valid,
#[serde(rename = "invalid")]
Invalid,
#[serde(rename = "unknown")]
NotFound,
}
impl std::fmt::Display for IpRpkiValidationState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IpRpkiValidationState::Valid => write!(f, "valid"),
IpRpkiValidationState::Invalid => write!(f, "invalid"),
IpRpkiValidationState::NotFound => write!(f, "unknown"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpAsnRouteInfo {
pub asn: i64,
#[serde(rename(serialize = "prefix"))]
pub prefix: IpNet,
pub rpki: IpRpkiValidationState,
pub name: String,
pub country: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpInfo {
pub ip: String,
#[serde(rename(serialize = "location"))]
pub country: Option<String>,
#[serde(rename(serialize = "network"))]
pub asn: Option<IpAsnRouteInfo>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
pub enum IpOutputFormat {
#[default]
Json,
Pretty,
Text,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "cli", derive(clap::Args))]
pub struct IpLookupArgs {
#[cfg_attr(feature = "cli", clap(value_name = "IP"))]
pub ip: Option<IpAddr>,
#[cfg_attr(feature = "cli", clap(short, long))]
#[serde(default)]
pub simple: bool,
#[cfg_attr(feature = "cli", clap(short, long, default_value = "json"))]
#[serde(default)]
pub format: IpOutputFormat,
}
impl IpLookupArgs {
pub fn new(ip: IpAddr) -> Self {
Self {
ip: Some(ip),
simple: false,
format: IpOutputFormat::default(),
}
}
pub fn public_ip() -> Self {
Self::default()
}
pub fn with_simple(mut self, simple: bool) -> Self {
self.simple = simple;
self
}
pub fn with_format(mut self, format: IpOutputFormat) -> Self {
self.format = format;
self
}
}
const IP_INFO_API: &str = "https://api.bgpkit.com/v3/utils/ip";
pub struct IpLens;
impl IpLens {
pub fn new() -> Self {
Self
}
pub fn lookup(&self, args: &IpLookupArgs) -> Result<IpInfo> {
let mut params = vec![];
if let Some(ip) = args.ip {
params.push(format!("ip={}", ip));
}
if args.simple {
params.push("simple=true".to_string());
}
let url = if params.is_empty() {
IP_INFO_API.to_string()
} else {
format!("{}?{}", IP_INFO_API, params.join("&"))
};
let resp = oneio::read_json_struct::<IpInfo>(&url)?;
Ok(resp)
}
pub fn format_result(&self, info: &IpInfo, format: &IpOutputFormat) -> String {
match format {
IpOutputFormat::Json => serde_json::to_string(info).unwrap_or_default(),
IpOutputFormat::Pretty => serde_json::to_string_pretty(info).unwrap_or_default(),
IpOutputFormat::Text => {
let mut lines = vec![format!("IP: {}", info.ip)];
if let Some(country) = &info.country {
lines.push(format!("Location: {}", country));
}
if let Some(asn) = &info.asn {
lines.push(format!("ASN: {}", asn.asn));
lines.push(format!("Prefix: {}", asn.prefix));
lines.push(format!("Name: {}", asn.name));
lines.push(format!("RPKI: {}", asn.rpki));
if let Some(country) = &asn.country {
lines.push(format!("AS Country: {}", country));
}
}
lines.join("\n")
}
}
}
}
impl Default for IpLens {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fetch_ip_info() {
let lens = IpLens::new();
let args = IpLookupArgs::public_ip();
let my_public_ip_info = lens.lookup(&args).unwrap();
dbg!(my_public_ip_info);
}
#[test]
fn test_format_text() {
let lens = IpLens::new();
let info = IpInfo {
ip: "1.1.1.1".to_string(),
country: Some("US".to_string()),
asn: Some(IpAsnRouteInfo {
asn: 13335,
prefix: "1.1.1.0/24".parse().unwrap(),
rpki: IpRpkiValidationState::Valid,
name: "CLOUDFLARENET".to_string(),
country: Some("US".to_string()),
}),
};
let output = lens.format_result(&info, &IpOutputFormat::Text);
assert!(output.contains("1.1.1.1"));
assert!(output.contains("13335"));
}
}