use crate::{
DriverCallback, DriverCategory, DriverContext, DriverError, DriverResult,
types::{Driver, DriverParameter},
};
use regex::Regex;
use reqwest::Client;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use tracing::{debug, info};
#[derive(Debug)]
pub struct IpInfoDriver;
#[async_trait::async_trait]
impl Driver for IpInfoDriver {
fn name(&self) -> &str {
"ip_info"
}
fn description(&self) -> &str {
"Get detailed information about an IP address including geolocation, ASN, ISP, and more"
}
fn usage_hint(&self) -> &str {
"Use this skill when the user wants to find location, ISP, or other information about an IP address. \
Supports both IPv4 and IPv6 addresses. If no IP is provided, returns information about the public IP."
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![DriverParameter {
name: "ip".to_string(),
param_type: "string".to_string(),
description: "IP address to lookup (IPv4 or IPv6). If omitted, returns your public IP".to_string(),
required: false,
default: None,
example: Some(Value::String("8.8.8.8".to_string())),
enum_values: None,
}];
}
fn example_call(&self) -> DriverResult<Value> {
return Ok(json!({
"action": "ip_info",
"parameters": {
"ip": "8.8.8.8"
}
}));
}
fn example_output(&self) -> String {
return "IP Information for 8.8.8.8:\nCountry: United States\nCity: Mountain View\nISP: Google LLC\nASN: AS15169\nLatitude: 37.4223\nLongitude: -122.0841\nTimezone: America/Los_Angeles".to_string();
}
fn category(&self) -> DriverCategory {
return DriverCategory::Network;
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing ip_info driver");
let ip_param = parameters.get("ip").and_then(|v| v.as_str());
let client = Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| DriverError::execution(format!("Failed to build HTTP client: {}", e)))?;
let url = if let Some(ip) = ip_param { format!("http://ip-api.com/json/{}", ip) } else { "http://ip-api.com/json".to_string() };
info!("IP info query: {}", url);
let response = client
.get(&url)
.header("User-Agent", "curl/7.68.0")
.send()
.await
.map_err(|e| DriverError::execution(format!("Failed to query IP info: {}", e)))?;
let text = response.text().await.map_err(|e| DriverError::execution(format!("Failed to read response: {}", e)))?;
let data: serde_json::Value = serde_json::from_str(&text).map_err(|e| DriverError::execution(format!("Failed to parse response: {}", e)))?;
if data.get("status").and_then(|s| s.as_str()) == Some("fail") {
let msg = data.get("message").and_then(|m| m.as_str()).unwrap_or("Unknown error");
info!("IP info query failed: {}", msg);
return Ok(format!("Failed to get IP info: {}", msg));
}
let ip_addr = ip_param.unwrap_or_else(|| data.get("query").and_then(|q| q.as_str()).unwrap_or("Unknown"));
info!("IP info retrieved for {}", ip_addr);
let mut result = String::new();
result.push_str(&format!("IP Information for {}:\n", ip_addr));
result.push_str(&format!("Country: {}\n", data.get("country").and_then(|v| v.as_str()).unwrap_or("N/A")));
result.push_str(&format!("Country Code: {}\n", data.get("countryCode").and_then(|v| v.as_str()).unwrap_or("N/A")));
result.push_str(&format!("Region: {}\n", data.get("regionName").and_then(|v| v.as_str()).unwrap_or("N/A")));
result.push_str(&format!("City: {}\n", data.get("city").and_then(|v| v.as_str()).unwrap_or("N/A")));
result.push_str(&format!("ZIP Code: {}\n", data.get("zip").and_then(|v| v.as_str()).unwrap_or("N/A")));
result.push_str(&format!("Latitude: {}\n", data.get("lat").and_then(|v| v.as_f64()).unwrap_or(0.0)));
result.push_str(&format!("Longitude: {}\n", data.get("lon").and_then(|v| v.as_f64()).unwrap_or(0.0)));
result.push_str(&format!("ISP: {}\n", data.get("isp").and_then(|v| v.as_str()).unwrap_or("N/A")));
result.push_str(&format!("Organization: {}\n", data.get("org").and_then(|v| v.as_str()).unwrap_or("N/A")));
result.push_str(&format!("AS: {}\n", data.get("as").and_then(|v| v.as_str()).unwrap_or("N/A")));
result.push_str(&format!("Timezone: {}\n", data.get("timezone").and_then(|v| v.as_str()).unwrap_or("N/A")));
return Ok(result);
}
}
#[derive(Debug)]
pub struct IpValidateDriver;
#[async_trait::async_trait]
impl Driver for IpValidateDriver {
fn name(&self) -> &str {
"ip_validate"
}
fn description(&self) -> &str {
"Validate IP addresses and provide information about their type (public/private, IPv4/IPv6, etc.)"
}
fn usage_hint(&self) -> &str {
"Use this skill to check if an IP address is valid and classify it (public, private, loopback, multicast, etc.)"
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![DriverParameter {
name: "ip".to_string(),
param_type: "string".to_string(),
description: "IP address to validate".to_string(),
required: true,
default: None,
example: Some(Value::String("192.168.1.1".to_string())),
enum_values: None,
}];
}
fn example_call(&self) -> DriverResult<Value> {
return Ok(json!({
"action": "ip_validate",
"parameters": {
"ip": "10.0.0.1"
}
}));
}
fn example_output(&self) -> String {
return "IP Address: 10.0.0.1\nType: IPv4\nClassification: Private (RFC 1918)\nValid: Yes\nLoopback: No\nMulticast: No".to_string();
}
fn category(&self) -> DriverCategory {
return DriverCategory::Network;
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing ip_validate driver");
let ip_str = parameters.get("ip").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("ip"))?;
info!("Validating IP: {}", ip_str);
match ip_str.parse::<IpAddr>() {
Ok(ip) => {
let mut result = format!("IP Address: {}\n", ip);
result.push_str(&format!("Type: {}\n", if ip.is_ipv4() { "IPv4" } else { "IPv6" }));
let classification = classify_ip(&ip);
result.push_str(&format!("Classification: {}\n", classification));
result.push_str("Valid: Yes\n");
result.push_str(&format!("Loopback: {}\n", ip.is_loopback()));
result.push_str(&format!("Multicast: {}\n", ip.is_multicast()));
if let IpAddr::V4(ipv4) = ip {
result.push_str(&format!("Broadcast: {}\n", ipv4.is_broadcast()));
result.push_str(&format!("Documentation: {}\n", is_documentation_ipv4(&ipv4)));
}
info!("IP validation successful: {}", ip_str);
return Ok(result);
}
Err(e) => {
info!("IP validation failed: {}", e);
return Ok(format!("Invalid IP address: {}\nError: {}", ip_str, e));
}
}
}
}
#[derive(Debug)]
pub struct IpRangeDriver;
#[async_trait::async_trait]
impl Driver for IpRangeDriver {
fn name(&self) -> &str {
"ip_range"
}
fn description(&self) -> &str {
"Calculate IP address ranges from CIDR notation or subnet mask"
}
fn usage_hint(&self) -> &str {
"Use this skill when you need to calculate network ranges, subnet details, or CIDR information"
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![DriverParameter {
name: "cidr".to_string(),
param_type: "string".to_string(),
description: "CIDR notation (e.g., 192.168.1.0/24) or IP with subnet mask".to_string(),
required: true,
default: None,
example: Some(Value::String("192.168.1.0/24".to_string())),
enum_values: None,
}];
}
fn example_call(&self) -> DriverResult<Value> {
return Ok(json!({
"action": "ip_range",
"parameters": {
"cidr": "10.0.0.0/16"
}
}));
}
fn example_output(&self) -> String {
return "CIDR: 10.0.0.0/16\nNetwork: 10.0.0.0\nSubnet Mask: 255.255.0.0\nWildcard: 0.0.255.255\nFirst IP: 10.0.0.1\nLast IP: 10.0.255.254\nBroadcast: 10.0.255.255\nTotal Hosts: 65534".to_string();
}
fn category(&self) -> DriverCategory {
return DriverCategory::Network;
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing ip_range driver");
let cidr = parameters.get("cidr").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("cidr"))?;
info!("Calculating IP range for CIDR: {}", cidr);
let re = Regex::new(r"^(\d+\.\d+\.\d+\.\d+)/(\d+)$").map_err(|e| DriverError::execution(format!("Invalid regex: {}", e)))?;
let caps = match re.captures(cidr) {
Some(caps) => caps,
None => return Ok(format!("Invalid CIDR format: {}", cidr)),
};
let ip_str = caps.get(1).unwrap().as_str();
let prefix_len: u32 = caps.get(2).unwrap().as_str().parse().map_err(|e| DriverError::execution(format!("Invalid prefix length: {}", e)))?;
if prefix_len > 32 {
return Err(DriverError::execution("Prefix length must be between 0 and 32"));
}
let ip: u32 = ip_str.parse::<Ipv4Addr>().map_err(|e| DriverError::execution(format!("Invalid IP address: {}", e)))?.into();
let mask = !((1 << (32 - prefix_len)) - 1);
let network = ip & mask;
let broadcast = network | !mask;
let first_host = if network == broadcast { network } else { network + 1 };
let last_host = if network == broadcast { broadcast } else { broadcast - 1 };
let total_hosts = if network == broadcast { 1 } else { broadcast - network - 1 };
info!("IP range calculated: network={}, broadcast={}, hosts={}", Ipv4Addr::from(network), Ipv4Addr::from(broadcast), total_hosts);
return Ok(format!(
"CIDR: {}\nNetwork: {}\nSubnet Mask: {}\nWildcard: {}\nFirst IP: {}\nLast IP: {}\nBroadcast: {}\nTotal Hosts: {}",
cidr,
Ipv4Addr::from(network),
Ipv4Addr::from(mask),
Ipv4Addr::from(!mask),
Ipv4Addr::from(first_host),
Ipv4Addr::from(last_host),
Ipv4Addr::from(broadcast),
total_hosts
));
}
}
#[derive(Debug)]
pub struct LocalIpDriver;
#[async_trait::async_trait]
impl Driver for LocalIpDriver {
fn name(&self) -> &str {
"local_ip"
}
fn description(&self) -> &str {
"Get local IP addresses of the current machine (both IPv4 and IPv6)"
}
fn usage_hint(&self) -> &str {
"Use this skill when you need to know the local IP addresses of the current system"
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![DriverParameter {
name: "type".to_string(),
param_type: "string".to_string(),
description: "IP type to return: 'all', 'ipv4', or 'ipv6'".to_string(),
required: false,
default: Some(Value::String("all".to_string())),
example: Some(Value::String("ipv4".to_string())),
enum_values: Some(vec!["all".to_string(), "ipv4".to_string(), "ipv6".to_string()]),
}];
}
fn example_call(&self) -> DriverResult<Value> {
return Ok(json!({
"action": "local_ip",
"parameters": {
"type": "all"
}
}));
}
fn example_output(&self) -> String {
return "Local IP addresses:\nIPv4: 192.168.1.100\nIPv6: fe80::1%eth0\nLoopback: 127.0.0.1".to_string();
}
fn category(&self) -> DriverCategory {
return DriverCategory::Network;
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing local_ip driver");
let ip_type = parameters.get("type").and_then(|v| v.as_str()).unwrap_or("all");
info!("Getting local IP addresses, type={}", ip_type);
let mut result = String::from("Local IP addresses:\n");
let interfaces =
local_ip_address::list_afinet_netifas().map_err(|e| DriverError::execution(format!("Failed to list network interfaces: {}", e)))?;
let mut found = false;
for (name, ip) in interfaces {
match ip {
IpAddr::V4(ipv4) => {
if ip_type == "all" || ip_type == "ipv4" {
if !ipv4.is_loopback() {
result.push_str(&format!(" {} ({}): {}\n", name, "IPv4", ipv4));
found = true;
}
}
}
IpAddr::V6(ipv6) => {
if ip_type == "all" || ip_type == "ipv6" {
if !ipv6.is_loopback() {
result.push_str(&format!(" {} ({}): {}\n", name, "IPv6", ipv6));
found = true;
}
}
}
}
}
result.push_str(&format!(" Loopback: 127.0.0.1\n"));
if !found && ip_type != "loopback" {
result.push_str(" No non-loopback addresses found\n");
info!("No non-loopback IP addresses found");
} else {
info!("Local IP addresses retrieved successfully");
}
return Ok(result);
}
}
fn classify_ip(ip: &IpAddr) -> String {
match ip {
IpAddr::V4(ipv4) => {
if ipv4.is_private() {
"Private (RFC 1918)".to_string()
} else if ipv4.is_loopback() {
"Loopback".to_string()
} else if ipv4.is_multicast() {
"Multicast".to_string()
} else if ipv4.is_broadcast() {
"Broadcast".to_string()
} else if is_documentation_ipv4(ipv4) {
"Documentation/Reserved".to_string()
} else {
"Public".to_string()
}
}
IpAddr::V6(ipv6) => {
if ipv6.is_loopback() {
"Loopback".to_string()
} else if ipv6.is_multicast() {
"Multicast".to_string()
} else if ipv6.is_unique_local() {
"Unique Local (ULA)".to_string()
} else if is_link_local(ipv6) {
"Link Local".to_string()
} else {
"Global Unicast".to_string()
}
}
}
}
fn is_documentation_ipv4(ip: &Ipv4Addr) -> bool {
let octets = ip.octets();
match octets {
[192, 0, 2, _] => true, [198, 51, 100, _] => true, [203, 0, 113, _] => true, [192, 88, 99, _] => true, [198, 18, 0, _] => true, _ => false,
}
}
fn is_link_local(ip: &Ipv6Addr) -> bool {
let segments = ip.segments();
segments[0] == 0xfe80 && (segments[1] & 0xffc0) == 0
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn test_classify_ipv4() {
let private_ips =
vec![Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(172, 16, 0, 1), Ipv4Addr::new(172, 31, 255, 255), Ipv4Addr::new(192, 168, 1, 1)];
for ip in private_ips {
assert_eq!(classify_ip(&IpAddr::V4(ip)), "Private (RFC 1918)");
}
let loopback = Ipv4Addr::new(127, 0, 0, 1);
assert_eq!(classify_ip(&IpAddr::V4(loopback)), "Loopback");
let multicast = Ipv4Addr::new(224, 0, 0, 1);
assert_eq!(classify_ip(&IpAddr::V4(multicast)), "Multicast");
let broadcast = Ipv4Addr::new(255, 255, 255, 255);
assert_eq!(classify_ip(&IpAddr::V4(broadcast)), "Broadcast");
let doc_ips = vec![
Ipv4Addr::new(192, 0, 2, 1), Ipv4Addr::new(198, 51, 100, 1), Ipv4Addr::new(203, 0, 113, 1), Ipv4Addr::new(192, 88, 99, 1), Ipv4Addr::new(198, 18, 0, 1), ];
for ip in doc_ips {
assert_eq!(classify_ip(&IpAddr::V4(ip)), "Documentation/Reserved");
}
let public_ips = vec![
Ipv4Addr::new(8, 8, 8, 8), Ipv4Addr::new(1, 1, 1, 1), Ipv4Addr::new(208, 67, 222, 222), ];
for ip in public_ips {
assert_eq!(classify_ip(&IpAddr::V4(ip)), "Public");
}
}
#[test]
fn test_classify_ipv6() {
let loopback = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1);
assert_eq!(classify_ip(&IpAddr::V6(loopback)), "Loopback");
let multicast = Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 1);
assert_eq!(classify_ip(&IpAddr::V6(multicast)), "Multicast");
let ula = Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1);
assert_eq!(classify_ip(&IpAddr::V6(ula)), "Unique Local (ULA)");
let link_local = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
assert_eq!(classify_ip(&IpAddr::V6(link_local)), "Link Local");
let global = Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888);
assert_eq!(classify_ip(&IpAddr::V6(global)), "Global Unicast");
}
#[test]
fn test_cidr_calculations() {
let ip: u32 = Ipv4Addr::new(192, 168, 1, 0).into();
let prefix_len = 24;
let mask = !((1 << (32 - prefix_len)) - 1);
let network = ip & mask;
let broadcast = network | !mask;
let total_hosts = broadcast - network - 1;
assert_eq!(network, u32::from(Ipv4Addr::new(192, 168, 1, 0)));
assert_eq!(broadcast, u32::from(Ipv4Addr::new(192, 168, 1, 255)));
assert_eq!(total_hosts, 254);
let ip: u32 = Ipv4Addr::new(10, 0, 0, 0).into();
let prefix_len = 16;
let mask = !((1 << (32 - prefix_len)) - 1);
let network = ip & mask;
let broadcast = network | !mask;
let total_hosts = broadcast - network - 1;
assert_eq!(network, u32::from(Ipv4Addr::new(10, 0, 0, 0)));
assert_eq!(broadcast, u32::from(Ipv4Addr::new(10, 0, 255, 255)));
assert_eq!(total_hosts, 65534);
let ip: u32 = Ipv4Addr::new(192, 168, 1, 100).into();
let prefix_len = 32;
let mask = !((1 << (32 - prefix_len)) - 1);
let network = ip & mask;
let broadcast = network | !mask;
let total_hosts = if network == broadcast { 1 } else { broadcast - network - 1 };
assert_eq!(network, u32::from(Ipv4Addr::new(192, 168, 1, 100)));
assert_eq!(broadcast, u32::from(Ipv4Addr::new(192, 168, 1, 100)));
assert_eq!(total_hosts, 1);
}
#[test]
fn test_link_local_detection() {
let valid = vec![
Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1),
Ipv6Addr::new(0xfe80, 0x1234, 0, 0, 0, 0, 0, 1),
Ipv6Addr::new(0xfe80, 0xffff, 0, 0, 0, 0, 0, 1),
];
for ip in valid {
assert!(is_link_local(&ip), "Expected {:?} to be link-local", ip);
}
let invalid = vec![
Ipv6Addr::new(0xfe90, 0, 0, 0, 0, 0, 0, 1), Ipv6Addr::new(0xfe00, 0, 0, 0, 0, 0, 0, 1), Ipv6Addr::new(0x2001, 0x4860, 0, 0, 0, 0, 0, 1), Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1), ];
for ip in invalid {
assert!(!is_link_local(&ip), "Expected {:?} not to be link-local", ip);
}
}
#[test]
fn test_ip_validation() {
let valid_v4 = vec!["8.8.8.8", "192.168.1.1", "10.0.0.1", "127.0.0.1"];
for ip in valid_v4 {
assert!(ip.parse::<IpAddr>().is_ok(), "Expected {} to be valid", ip);
}
let invalid_v4 = vec!["256.1.1.1", "1.1.1", "abc.def.ghi.jkl", "192.168.1.256"];
for ip in invalid_v4 {
assert!(ip.parse::<IpAddr>().is_err(), "Expected {} to be invalid", ip);
}
let valid_v6 = vec!["::1", "2001:4860:4860::8888", "fe80::1", "fc00::1", "ff00::1"];
for ip in valid_v6 {
assert!(ip.parse::<IpAddr>().is_ok(), "Expected {} to be valid", ip);
}
let invalid_v6 = vec![":::", "2001:4860:4860::8888::1", "fe80:1"];
for ip in invalid_v6 {
assert!(ip.parse::<IpAddr>().is_err(), "Expected {} to be invalid", ip);
}
}
#[tokio::test]
async fn test_ip_info_driver() {
let skill = IpInfoDriver;
let mut params = HashMap::new();
params.insert("ip".to_string(), json!("8.8.8.8"));
let result = skill.execute(¶ms, None, None).await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("IP Information for 8.8.8.8:"));
assert!(output.contains("Country:"));
assert!(output.contains("ISP:"));
assert!(output.contains("AS:"));
}
#[tokio::test]
async fn test_local_ip_driver() {
let skill = LocalIpDriver;
let mut params = HashMap::new();
params.insert("type".to_string(), json!("all"));
let result = skill.execute(¶ms, None, None).await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("Local IP addresses:"));
assert!(output.contains("Loopback: 127.0.0.1"));
}
#[tokio::test]
async fn test_ip_range_driver() {
let skill = IpRangeDriver;
let mut params = HashMap::new();
params.insert("cidr".to_string(), json!("192.168.1.0/24"));
let result = skill.execute(¶ms, None, None).await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("CIDR: 192.168.1.0/24"));
assert!(output.contains("Network: 192.168.1.0"));
assert!(output.contains("Broadcast: 192.168.1.255"));
assert!(output.contains("Total Hosts: 254"));
}
#[tokio::test]
async fn test_ip_range_driver_invalid() {
let skill = IpRangeDriver;
let mut params = HashMap::new();
params.insert("cidr".to_string(), json!("invalid"));
let result = skill.execute(¶ms, None, None).await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("Invalid CIDR format"));
}
}