use crate::types::{PortalUrl, Source};
const DEFAULT_LEASE_FILES: &[&str] = &[
"/var/lib/dhcp/dhclient.leases", "/var/lib/dhclient/dhclient.leases", "/var/lib/dhclient/dhclient6.leases",
"/var/lib/NetworkManager/dhclient.conf", ];
pub async fn detect(files: &[String]) -> Vec<PortalUrl> {
detect_inner(files).await.unwrap_or_default()
}
async fn detect_inner(
files: &[String],
) -> Result<Vec<PortalUrl>, Box<dyn std::error::Error + Send + Sync>> {
let candidates: Vec<&str> = if files.is_empty() {
DEFAULT_LEASE_FILES.to_vec()
} else {
files.iter().map(String::as_str).collect()
};
let mut results = Vec::new();
for path in &candidates {
if let Ok(content) = tokio::fs::read_to_string(path).await {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("option dhcp-captive-portal-uri") {
if let Some(url) = extract_quoted(trimmed) {
if url::Url::parse(url).is_ok() {
results.push(PortalUrl {
url: url.to_owned(),
source: Source::DhcpLease,
confidence: 95,
});
}
}
}
}
}
}
results.dedup_by(|a, b| a.url == b.url);
Ok(results)
}
fn extract_quoted(line: &str) -> Option<&str> {
let start = line.find('"')? + 1;
let end = line.rfind('"')?;
if start < end { Some(&line[start..end]) } else { None }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_quoted_uri() {
let line = r#" option dhcp-captive-portal-uri "http://192.168.1.1/portal";"#;
assert_eq!(extract_quoted(line), Some("http://192.168.1.1/portal"));
}
#[test]
fn empty_string_returns_none() {
assert_eq!(extract_quoted("option dhcp-captive-portal-uri"), None);
}
}