use crate::{ContentRouting, ProviderInfo, RoutingError};
use async_trait::async_trait;
use cid::Cid;
use libp2p::{Multiaddr, PeerId};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, warn};
use url::Url;
const DEFAULT_ENDPOINTS: &[&str] = &["https://cid.contact", "https://delegated-ipfs.dev"];
#[derive(Debug, Clone)]
pub struct DelegatedHTTPRoutingInit {
pub endpoints: Vec<Url>,
pub timeout_ms: u64,
pub max_providers: usize,
}
impl Default for DelegatedHTTPRoutingInit {
fn default() -> Self {
Self {
endpoints: DEFAULT_ENDPOINTS
.iter()
.filter_map(|url| Url::parse(url).ok())
.collect(),
timeout_ms: 30000, max_providers: 20,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct ProviderRecord {
#[serde(default)]
protocol: String,
#[serde(default)]
schema: String,
#[serde(rename = "ID")]
id: Option<String>,
#[serde(default)]
addrs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct RoutingResponse {
#[serde(default)]
providers: Vec<ProviderRecord>,
}
pub struct DelegatedHTTPRouter {
client: Client,
endpoints: Vec<Url>,
config: DelegatedHTTPRoutingInit,
}
impl DelegatedHTTPRouter {
pub fn new(init: DelegatedHTTPRoutingInit) -> Self {
let client = Client::builder()
.timeout(Duration::from_millis(init.timeout_ms))
.build()
.expect("Failed to create HTTP client");
Self {
client,
endpoints: init.endpoints.clone(),
config: init,
}
}
async fn query_endpoint(
&self,
endpoint: &Url,
cid: &Cid,
) -> Result<Vec<ProviderInfo>, RoutingError> {
let mut url = endpoint.clone();
url.set_path(&format!("/routing/v1/providers/{}", cid));
debug!("Querying delegated routing endpoint: {}", url);
let response = self
.client
.get(url.clone())
.header("Accept", "application/json")
.send()
.await
.map_err(|e| {
warn!("Failed to query {}: {}", url, e);
RoutingError::RoutingFailed(format!("HTTP request failed: {}", e))
})?;
if !response.status().is_success() {
warn!("Endpoint returned error status: {}", response.status());
return Err(RoutingError::RoutingFailed(format!(
"HTTP {}",
response.status()
)));
}
let routing_response: RoutingResponse = response.json().await.map_err(|e| {
warn!("Failed to parse response from {}: {}", url, e);
RoutingError::RoutingFailed(format!("Invalid JSON response: {}", e))
})?;
debug!(
"Found {} provider records from {}",
routing_response.providers.len(),
url
);
let mut providers = Vec::new();
for record in routing_response
.providers
.iter()
.take(self.config.max_providers)
{
let peer_id = if let Some(id_str) = &record.id {
match PeerId::from_str(id_str) {
Ok(pid) => pid,
Err(e) => {
warn!("Invalid peer ID '{}': {}", id_str, e);
continue;
}
}
} else {
warn!("Provider record missing ID field");
continue;
};
let mut addrs = Vec::new();
for addr_str in &record.addrs {
match Multiaddr::from_str(addr_str) {
Ok(addr) => addrs.push(addr),
Err(e) => {
warn!("Invalid multiaddr '{}': {}", addr_str, e);
}
}
}
if addrs.is_empty() {
debug!("Provider {} has no valid addresses, skipping", peer_id);
continue;
}
providers.push(ProviderInfo { peer_id, addrs });
}
Ok(providers)
}
}
#[async_trait]
impl ContentRouting for DelegatedHTTPRouter {
async fn find_providers(&self, cid: &Cid) -> Result<Vec<ProviderInfo>, RoutingError> {
let mut all_providers = Vec::new();
let mut last_error = None;
for endpoint in &self.endpoints {
match self.query_endpoint(endpoint, cid).await {
Ok(mut providers) => {
debug!("Got {} providers from {}", providers.len(), endpoint);
all_providers.append(&mut providers);
}
Err(e) => {
warn!("Endpoint {} failed: {}", endpoint, e);
last_error = Some(e);
}
}
}
if !all_providers.is_empty() {
all_providers.sort_by_key(|p| p.peer_id);
all_providers.dedup_by_key(|p| p.peer_id);
all_providers.truncate(self.config.max_providers);
debug!("Returning {} unique providers", all_providers.len());
return Ok(all_providers);
}
Err(last_error.unwrap_or_else(|| RoutingError::ContentNotFound(*cid)))
}
async fn provide(&self, _cid: &Cid) -> Result<(), RoutingError> {
Err(RoutingError::RoutingFailed(
"Delegated HTTP routing does not support content announcement".to_string(),
))
}
}
pub fn delegated_http_routing(init: DelegatedHTTPRoutingInit) -> Arc<dyn ContentRouting> {
Arc::new(DelegatedHTTPRouter::new(init))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_delegated_router_creation() {
let router = DelegatedHTTPRouter::new(DelegatedHTTPRoutingInit::default());
assert!(!router.endpoints.is_empty());
}
#[tokio::test]
async fn test_provide_not_supported() {
let router = delegated_http_routing(DelegatedHTTPRoutingInit::default());
let cid = Cid::default();
let result = router.provide(&cid).await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore] async fn test_find_providers_real() {
let cid =
Cid::try_from("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi").unwrap();
let router = delegated_http_routing(DelegatedHTTPRoutingInit {
timeout_ms: 10000,
..Default::default()
});
match router.find_providers(&cid).await {
Ok(providers) => {
println!("Found {} providers", providers.len());
for provider in &providers {
println!(
" Provider: {} with {} addrs",
provider.peer_id,
provider.addrs.len()
);
}
}
Err(e) => {
println!("Warning: Failed to find providers: {}", e);
}
}
}
}