use async_trait::async_trait;
use cid::Cid;
use futures::stream;
use helia_interface::{
AwaitIterable, FindPeersOptions, FindProvidersOptions, GetOptions, HeliaError, PeerInfo,
ProvideOptions, Provider, PutOptions, Routing, RoutingRecord, TransportMethod,
};
use libp2p::{Multiaddr, PeerId};
use std::str::FromStr;
use tracing::debug;
use url::Url;
#[derive(Debug, Clone)]
pub struct HTTPGatewayRoutingInit {
pub gateways: Vec<Url>,
}
impl Default for HTTPGatewayRoutingInit {
fn default() -> Self {
Self {
gateways: vec![
Url::parse("https://ipfs.io").unwrap(),
Url::parse("https://dweb.link").unwrap(),
Url::parse("https://cloudflare-ipfs.com").unwrap(),
],
}
}
}
pub struct HTTPGatewayRouter {
gateways: Vec<Url>,
}
impl HTTPGatewayRouter {
pub fn new(init: HTTPGatewayRoutingInit) -> Self {
Self {
gateways: init.gateways,
}
}
fn gateway_to_peer_id(gateway: &Url) -> PeerId {
let host = gateway.host_str().unwrap_or("unknown");
let hash = seahash::hash(host.as_bytes());
let peer_id_bytes = format!("12D3KooW{:032x}", hash);
PeerId::from_str(&peer_id_bytes).unwrap_or_else(|_| {
PeerId::random()
})
}
fn gateway_to_multiaddr(gateway: &Url) -> Option<Multiaddr> {
let host = gateway.host_str()?;
let port = gateway
.port()
.unwrap_or(if gateway.scheme() == "https" { 443 } else { 80 });
let protocol = if gateway.scheme() == "https" {
"https"
} else {
"http"
};
let multiaddr_str = format!("/dns4/{}/tcp/{}/{}", host, port, protocol);
Multiaddr::from_str(&multiaddr_str).ok()
}
fn gateway_to_provider(&self, gateway: &Url) -> Option<Provider> {
let peer_id = Self::gateway_to_peer_id(gateway);
let addr = Self::gateway_to_multiaddr(gateway)?;
Some(Provider {
peer_info: PeerInfo {
id: peer_id,
multiaddrs: vec![addr],
protocols: vec!["http".to_string()],
},
transport_methods: vec![TransportMethod::Http],
})
}
}
#[async_trait]
impl Routing for HTTPGatewayRouter {
async fn find_providers(
&self,
cid: &Cid,
_options: Option<FindProvidersOptions>,
) -> Result<AwaitIterable<Provider>, HeliaError> {
debug!(
"HTTPGatewayRouter: Returning {} gateways as providers for {}",
self.gateways.len(),
cid
);
let mut providers = Vec::new();
for gateway in &self.gateways {
if let Some(provider) = self.gateway_to_provider(gateway) {
providers.push(provider);
}
}
if providers.is_empty() {
return Err(HeliaError::NotFound(format!(
"No valid gateway providers found for CID: {}",
cid
)));
}
Ok(Box::pin(stream::iter(providers)))
}
async fn provide(&self, _cid: &Cid, _options: Option<ProvideOptions>) -> Result<(), HeliaError> {
Err(HeliaError::OperationNotSupported(
"HTTP gateway routing does not support content announcement".to_string(),
))
}
async fn find_peers(
&self,
_peer_id: &PeerId,
_options: Option<FindPeersOptions>,
) -> Result<AwaitIterable<PeerInfo>, HeliaError> {
Err(HeliaError::OperationNotSupported(
"HTTP gateway routing does not support peer discovery".to_string(),
))
}
async fn get(
&self,
_key: &[u8],
_options: Option<GetOptions>,
) -> Result<Option<RoutingRecord>, HeliaError> {
Err(HeliaError::OperationNotSupported(
"HTTP gateway routing does not support DHT record retrieval".to_string(),
))
}
async fn put(
&self,
_key: &[u8],
_value: &[u8],
_options: Option<PutOptions>,
) -> Result<(), HeliaError> {
Err(HeliaError::OperationNotSupported(
"HTTP gateway routing does not support DHT record storage".to_string(),
))
}
}
pub fn http_gateway_routing(init: HTTPGatewayRoutingInit) -> Box<dyn Routing> {
Box::new(HTTPGatewayRouter::new(init))
}
#[cfg(test)]
mod tests {
use super::*;
use futures::StreamExt;
#[tokio::test]
async fn test_gateway_router_creation() {
let router = HTTPGatewayRouter::new(HTTPGatewayRoutingInit::default());
assert_eq!(router.gateways.len(), 3);
}
#[tokio::test]
async fn test_find_providers_returns_gateways() {
let router = http_gateway_routing(HTTPGatewayRoutingInit::default());
let cid = Cid::default();
let mut providers = router.find_providers(&cid, None).await.unwrap();
let provider_vec: Vec<_> = providers.collect().await;
assert_eq!(provider_vec.len(), 3);
for provider in &provider_vec {
assert!(!provider.peer_info.multiaddrs.is_empty());
assert_eq!(provider.transport_methods.len(), 1);
}
}
#[tokio::test]
async fn test_provide_not_supported() {
let router = http_gateway_routing(HTTPGatewayRoutingInit::default());
let cid = Cid::default();
let result = router.provide(&cid, None).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_custom_gateways() {
let custom_gateway = Url::parse("https://custom.gateway.example").unwrap();
let router = http_gateway_routing(HTTPGatewayRoutingInit {
gateways: vec![custom_gateway],
});
let cid = Cid::default();
let mut providers = router.find_providers(&cid, None).await.unwrap();
let provider_vec: Vec<_> = providers.collect().await;
assert_eq!(provider_vec.len(), 1);
}
#[tokio::test]
async fn test_peer_routing_not_supported() {
let router = http_gateway_routing(HTTPGatewayRoutingInit::default());
let peer_id = PeerId::random();
let result = router.find_peers(&peer_id, None).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_dht_operations_not_supported() {
let router = http_gateway_routing(HTTPGatewayRoutingInit::default());
let get_result = router.get(b"test-key", None).await;
assert!(get_result.is_err());
let put_result = router.put(b"test-key", b"test-value", None).await;
assert!(put_result.is_err());
}
}