use foundation_core::url::Uri;
use foundation_netio::default_http_client;
use foundation_netio::shared::client::body_reader::try_collect_bytes;
use foundation_netio::shared::client::request::PreparedRequest;
use foundation_netio::shared::http::{SendSafeBody, SimpleHeader, SimpleHeaders, SimpleMethod};
use crate::core::error::{AppError, AppResult};
use crate::core::util::validate_icon_domain;
#[derive(Debug, Clone)]
pub struct IconData {
pub content_type: String,
pub bytes: Vec<u8>,
}
fn is_plausible_domain(domain: &str) -> bool {
!domain.is_empty()
&& domain.len() <= 253
&& !domain.contains('/')
&& !domain.contains('@')
&& !domain.contains(':')
&& !domain.contains("..")
&& domain.contains('.')
&& domain.parse::<std::net::IpAddr>().is_err()
}
pub async fn fetch_icon(domain: &str) -> AppResult<IconData> {
let domain = domain.trim().to_lowercase();
if !is_plausible_domain(&domain) {
return Err(AppError::BadRequest("invalid icon domain".into()));
}
let url = format!("https://{domain}/favicon.ico");
if !validate_icon_domain(&url) {
return Err(AppError::Forbidden);
}
let client = default_http_client();
let uri = Uri::parse(&url).map_err(|e| AppError::BadRequest(format!("invalid domain URL: {e}")))?;
if uri.host_str().as_deref() != Some(domain.as_str()) || uri.port().is_some() {
return Err(AppError::Forbidden);
}
let mut headers = SimpleHeaders::new();
headers.insert(SimpleHeader::ACCEPT, vec!["image/*".into()]);
let req = PreparedRequest {
method: SimpleMethod::GET,
url: uri,
headers,
body: SendSafeBody::None,
extensions: Default::default(),
};
let resp = client
.send_async(req)
.await
.map_err(|e| AppError::NotFound(format!("icon fetch failed: {e}")))?;
let status: usize = resp.get_status().into();
if !(200..300).contains(&status) {
return Err(AppError::NotFound(format!("icon not found (HTTP {status})")));
}
let content_type = resp
.get_headers_ref()
.get(&SimpleHeader::CONTENT_TYPE)
.and_then(|v| v.first())
.cloned()
.unwrap_or_else(|| "image/x-icon".to_string());
let bytes = try_collect_bytes(resp.take_body())
.map_err(|e| AppError::NotFound(format!("read icon body: {e}")))?;
Ok(IconData { content_type, bytes })
}