use crate::Result;
use crate::error::TlsError;
use ipnetwork::IpNetwork;
use std::net::IpAddr;
pub struct AsnCidrParser;
impl AsnCidrParser {
pub async fn expand_asn(asn: &str) -> Result<Vec<IpNetwork>> {
let asn_number = Self::parse_asn_number(asn)?;
let prefixes = Self::query_ripestat_api(asn_number).await?;
Ok(prefixes)
}
fn parse_asn_number(asn: &str) -> Result<u32> {
let asn_str = asn.trim();
let num_str = if asn_str.to_uppercase().starts_with("AS") {
&asn_str[2..]
} else {
asn_str
};
num_str.parse::<u32>().map_err(|e| TlsError::InvalidInput {
message: format!("Invalid ASN format '{}': {}", asn, e),
})
}
async fn query_ripestat_api(asn: u32) -> Result<Vec<IpNetwork>> {
let url = format!(
"https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS{}",
asn
);
let client = reqwest::Client::builder()
.user_agent("CipherRun/1.0")
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| TlsError::ConfigError {
message: format!("Failed to create HTTP client: {}", e),
})?;
let response = client
.get(&url)
.send()
.await
.map_err(|e| TlsError::HttpError {
status: 0,
details: format!("RIPEstat API request failed: {}", e),
})?;
if !response.status().is_success() {
return Err(TlsError::HttpError {
status: response.status().as_u16(),
details: format!("RIPEstat API returned error: {}", response.status()),
});
}
let json: serde_json::Value = response.json().await.map_err(|e| TlsError::ParseError {
message: format!("Failed to parse RIPEstat response: {}", e),
})?;
let mut prefixes = Vec::new();
if let Some(data) = json.get("data")
&& let Some(prefixes_array) = data.get("prefixes").and_then(|p| p.as_array())
{
for prefix_obj in prefixes_array {
if let Some(prefix_str) = prefix_obj.get("prefix").and_then(|p| p.as_str())
&& let Ok(network) = prefix_str.parse::<IpNetwork>()
{
prefixes.push(network);
}
}
}
if prefixes.is_empty() {
return Err(TlsError::InvalidInput {
message: format!("No prefixes found for AS{}", asn),
});
}
Ok(prefixes)
}
pub fn expand_cidr(cidr: &str) -> Result<CidrExpansion> {
let network = cidr
.parse::<IpNetwork>()
.map_err(|e| TlsError::InvalidInput {
message: format!("Invalid CIDR format '{}': {}", cidr, e),
})?;
let total_ips = Self::calculate_ip_count(&network);
const MAX_EXPAND: u64 = 1024;
if total_ips <= MAX_EXPAND {
let ips: Vec<IpAddr> = network.iter().collect();
Ok(CidrExpansion::FullList {
network,
ips,
total: total_ips,
})
} else {
Ok(CidrExpansion::Network {
network,
total: total_ips,
})
}
}
fn calculate_ip_count(network: &IpNetwork) -> u64 {
match network {
IpNetwork::V4(v4) => {
let prefix_len = v4.prefix();
if prefix_len >= 32 {
1
} else {
2u64.pow(32 - prefix_len as u32)
}
}
IpNetwork::V6(v6) => {
let prefix_len = v6.prefix();
if prefix_len >= 128 {
1
} else if prefix_len <= 64 {
u64::MAX
} else {
2u64.pow(128 - prefix_len as u32)
}
}
}
}
pub fn parse_input(input: &str) -> InputType {
let input = input.trim();
if input.to_uppercase().starts_with("AS") || input.parse::<u32>().is_ok() {
if let Ok(asn_num) = Self::parse_asn_number(input)
&& asn_num > 0
&& asn_num < 4_294_967_295
{
return InputType::Asn(input.to_string());
}
}
if input.contains('/') && input.parse::<IpNetwork>().is_ok() {
return InputType::Cidr(input.to_string());
}
if let Ok(ip) = input.parse::<IpAddr>() {
return InputType::Ip(ip);
}
InputType::Hostname(input.to_string())
}
pub async fn expand_inputs(inputs: Vec<String>) -> Result<Vec<ExpandedInput>> {
let mut results = Vec::new();
for input in inputs {
let input_type = Self::parse_input(&input);
let expanded = Self::expand_input_type(input_type).await?;
results.push(expanded);
}
Ok(results)
}
async fn expand_input_type(input_type: InputType) -> Result<ExpandedInput> {
match input_type {
InputType::Asn(asn) => {
let networks = Self::expand_asn(&asn).await?;
Ok(ExpandedInput::Asn {
asn: asn.clone(),
networks,
})
}
InputType::Cidr(cidr) => {
let expansion = Self::expand_cidr(&cidr)?;
Ok(ExpandedInput::Cidr {
cidr: cidr.clone(),
expansion,
})
}
InputType::Ip(ip) => Ok(ExpandedInput::Ip { ip }),
InputType::Hostname(hostname) => Ok(ExpandedInput::Hostname { hostname }),
}
}
}
#[derive(Debug, Clone)]
pub enum InputType {
Asn(String),
Cidr(String),
Ip(IpAddr),
Hostname(String),
}
#[derive(Debug, Clone)]
pub enum CidrExpansion {
FullList {
network: IpNetwork,
ips: Vec<IpAddr>,
total: u64,
},
Network { network: IpNetwork, total: u64 },
}
impl CidrExpansion {
pub fn iter(&self) -> Box<dyn Iterator<Item = IpAddr> + '_> {
match self {
CidrExpansion::FullList { ips, .. } => Box::new(ips.iter().copied()),
CidrExpansion::Network { network, .. } => Box::new(network.iter()),
}
}
pub fn total_ips(&self) -> u64 {
match self {
CidrExpansion::FullList { total, .. } => *total,
CidrExpansion::Network { total, .. } => *total,
}
}
pub fn network(&self) -> &IpNetwork {
match self {
CidrExpansion::FullList { network, .. } => network,
CidrExpansion::Network { network, .. } => network,
}
}
}
#[derive(Debug, Clone)]
pub enum ExpandedInput {
Asn {
asn: String,
networks: Vec<IpNetwork>,
},
Cidr {
cidr: String,
expansion: CidrExpansion,
},
Ip {
ip: IpAddr,
},
Hostname {
hostname: String,
},
}
impl ExpandedInput {
pub fn target_count(&self) -> u64 {
match self {
ExpandedInput::Asn { networks, .. } => networks.iter().map(Self::network_size).sum(),
ExpandedInput::Cidr { expansion, .. } => expansion.total_ips(),
ExpandedInput::Ip { .. } => 1,
ExpandedInput::Hostname { .. } => 1,
}
}
fn network_size(network: &IpNetwork) -> u64 {
match network {
IpNetwork::V4(v4) => {
let prefix = v4.prefix();
if prefix >= 32 {
1
} else {
2u64.pow(32 - prefix as u32)
}
}
IpNetwork::V6(v6) => {
let prefix = v6.prefix();
if prefix >= 128 {
1
} else if prefix <= 64 {
u64::MAX
} else {
2u64.pow(128 - prefix as u32)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_asn_number() {
assert_eq!(AsnCidrParser::parse_asn_number("1449").unwrap(), 1449);
assert_eq!(AsnCidrParser::parse_asn_number("AS1449").unwrap(), 1449);
assert_eq!(AsnCidrParser::parse_asn_number("as1449").unwrap(), 1449);
assert!(AsnCidrParser::parse_asn_number("invalid").is_err());
assert!(AsnCidrParser::parse_asn_number("AS").is_err());
}
#[test]
fn test_parse_input_asn() {
match AsnCidrParser::parse_input("AS1449") {
InputType::Asn(asn) => assert_eq!(asn, "AS1449"),
_ => panic!("Expected ASN input type"),
}
match AsnCidrParser::parse_input("1449") {
InputType::Asn(asn) => assert_eq!(asn, "1449"),
_ => panic!("Expected ASN input type"),
}
}
#[test]
fn test_parse_input_cidr() {
match AsnCidrParser::parse_input("192.0.2.0/24") {
InputType::Cidr(cidr) => assert_eq!(cidr, "192.0.2.0/24"),
_ => panic!("Expected CIDR input type"),
}
match AsnCidrParser::parse_input("2001:db8::/32") {
InputType::Cidr(cidr) => assert_eq!(cidr, "2001:db8::/32"),
_ => panic!("Expected CIDR input type"),
}
}
#[test]
fn test_parse_input_ip() {
match AsnCidrParser::parse_input("192.0.2.1") {
InputType::Ip(ip) => assert_eq!(ip.to_string(), "192.0.2.1"),
_ => panic!("Expected IP input type"),
}
match AsnCidrParser::parse_input("2001:db8::1") {
InputType::Ip(ip) => assert!(ip.to_string().contains("2001:db8")),
_ => panic!("Expected IP input type"),
}
}
#[test]
fn test_parse_input_hostname() {
match AsnCidrParser::parse_input("example.com") {
InputType::Hostname(hostname) => assert_eq!(hostname, "example.com"),
_ => panic!("Expected Hostname input type"),
}
}
#[test]
fn test_expand_cidr_small() {
let expansion =
AsnCidrParser::expand_cidr("192.0.2.0/30").expect("test assertion should succeed");
match expansion {
CidrExpansion::FullList { ips, total, .. } => {
assert_eq!(total, 4);
assert_eq!(ips.len(), 4);
}
_ => panic!("Expected FullList for small network"),
}
}
#[test]
fn test_expand_cidr_large() {
let expansion =
AsnCidrParser::expand_cidr("10.0.0.0/8").expect("test assertion should succeed");
match expansion {
CidrExpansion::Network { total, .. } => {
assert_eq!(total, 16777216); }
_ => panic!("Expected Network for large network"),
}
}
#[test]
fn test_cidr_expansion_iter() {
let expansion =
AsnCidrParser::expand_cidr("192.0.2.0/30").expect("test assertion should succeed");
let ips: Vec<IpAddr> = expansion.iter().collect();
assert_eq!(ips.len(), 4);
assert_eq!(ips[0].to_string(), "192.0.2.0");
assert_eq!(ips[3].to_string(), "192.0.2.3");
}
}