use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use futures::future::BoxFuture;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use indexmap::IndexSet;
use ipld_core::cid::Cid;
use parking_lot::RwLock;
use serde::Deserialize;
use connexa::handle::Connexa;
use connexa::prelude::swarm::dial_opts::DialOpts;
use connexa::prelude::{Multiaddr, MultiaddrExt, PeerId};
use crate::repo::DefaultKeystore;
use crate::IpfsEvent;
type IpfsConnexa = Connexa<IpfsEvent, DefaultKeystore>;
const DEFAULT_ROUTERS: &[&str] = &["https://delegated-ipfs.dev"];
const MAX_INFLIGHT: usize = 8;
const MAX_PROVIDERS: usize = 8;
const MAX_ADDRS_PER_RECORD: usize = 8;
const MAX_RESPONSE_SIZE: usize = 1024 * 1024;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
#[derive(Clone)]
pub struct RouterList {
inner: Arc<RwLock<IndexSet<String>>>,
}
impl RouterList {
pub(crate) fn new<I, S>(routers: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut set: IndexSet<String> = routers.into_iter().map(|r| normalize(&r.into())).collect();
if set.is_empty() {
set = DEFAULT_ROUTERS.iter().map(|r| r.to_string()).collect();
}
Self {
inner: Arc::new(RwLock::new(set)),
}
}
pub(crate) fn add(&self, url: &str) -> bool {
self.inner.write().insert(normalize(url))
}
pub(crate) fn remove(&self, url: &str) -> bool {
self.inner.write().shift_remove(&normalize(url))
}
pub(crate) fn list(&self) -> Vec<String> {
self.inner.read().iter().cloned().collect()
}
}
fn normalize(url: &str) -> String {
url.trim_end_matches('/').to_string()
}
#[derive(Deserialize)]
struct ProvidersResponse {
#[serde(rename = "Providers", default)]
providers: Vec<ProviderRecord>,
}
#[derive(Deserialize)]
struct ProviderRecord {
#[serde(rename = "ID")]
id: Option<PeerId>,
#[serde(rename = "Addrs", default)]
addrs: Vec<Multiaddr>,
}
pub(crate) async fn run(
mut rx: futures::channel::mpsc::Receiver<Cid>,
connexa: IpfsConnexa,
routers: RouterList,
) {
let client = match reqwest::Client::builder().build() {
Ok(client) => client,
Err(error) => {
error!(%error, "failed to build delegated routing http client. disabling delegated routing.");
return;
}
};
let mut inflight: HashSet<Cid> = HashSet::new();
let mut lookups: FuturesUnordered<BoxFuture<'static, Cid>> = FuturesUnordered::new();
loop {
tokio::select! {
maybe_cid = rx.next() => {
let Some(cid) = maybe_cid else { break };
if !inflight.insert(cid) {
continue;
}
if lookups.len() >= MAX_INFLIGHT {
inflight.remove(&cid);
continue;
}
lookups.push(boxed_lookup(client.clone(), routers.list(), cid, connexa.clone()));
}
Some(cid) = lookups.next(), if !lookups.is_empty() => {
inflight.remove(&cid);
}
}
}
}
fn boxed_lookup(
client: reqwest::Client,
routers: Vec<String>,
cid: Cid,
connexa: IpfsConnexa,
) -> BoxFuture<'static, Cid> {
let fut = async move {
lookup_and_dial(&client, &routers, cid, &connexa).await;
cid
};
#[cfg(target_arch = "wasm32")]
{
Box::pin(send_wrapper::SendWrapper::new(fut))
}
#[cfg(not(target_arch = "wasm32"))]
{
Box::pin(fut)
}
}
async fn lookup_and_dial(
client: &reqwest::Client,
routers: &[String],
cid: Cid,
connexa: &IpfsConnexa,
) {
for router in routers {
let url = format!("{router}/routing/v1/providers/{cid}");
let Ok(response) = client
.get(&url)
.header("Accept", "application/json")
.timeout(REQUEST_TIMEOUT)
.send()
.await
else {
continue;
};
if !response.status().is_success() {
continue;
}
let Some(bytes) = read_capped(response).await else {
continue;
};
let Ok(body) = serde_json::from_slice::<ProvidersResponse>(&bytes) else {
continue;
};
let mut dialed = 0usize;
for record in body.providers {
let Some(peer) = record.id else {
continue;
};
let addrs: Vec<Multiaddr> = record
.addrs
.iter()
.filter(|&addr| addr.is_public())
.take(MAX_ADDRS_PER_RECORD)
.cloned()
.collect();
if addrs.is_empty() {
continue;
}
let _ = connexa
.swarm()
.dial(DialOpts::peer_id(peer).addresses(addrs).build())
.await;
dialed += 1;
if dialed >= MAX_PROVIDERS {
break;
}
}
if dialed > 0 {
break;
}
}
}
async fn read_capped(response: reqwest::Response) -> Option<bytes::Bytes> {
if response
.content_length()
.is_some_and(|len| len > MAX_RESPONSE_SIZE as u64)
{
return None;
}
#[cfg(not(target_arch = "wasm32"))]
{
let mut response = response;
let mut buffer = Vec::new();
while let Some(chunk) = response.chunk().await.ok()? {
if buffer.len() + chunk.len() > MAX_RESPONSE_SIZE {
return None;
}
buffer.extend_from_slice(&chunk);
}
Some(bytes::Bytes::from(buffer))
}
#[cfg(target_arch = "wasm32")]
{
if response.content_length().is_none() {
return None;
}
let bytes = response.bytes().await.ok()?;
(bytes.len() <= MAX_RESPONSE_SIZE).then_some(bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_providers_response() {
let json = r#"{
"Providers": [
{ "Schema": "peer", "ID": "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN", "Addrs": ["/ip4/1.2.3.4/tcp/4001"] },
{ "Schema": "peer", "ID": "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN" }
]
}"#;
let body: ProvidersResponse = serde_json::from_str(json).unwrap();
assert_eq!(body.providers.len(), 2);
assert_eq!(
body.providers[0].addrs,
vec!["/ip4/1.2.3.4/tcp/4001".parse::<Multiaddr>().unwrap()]
);
assert!(body.providers[1].addrs.is_empty());
}
#[test]
fn router_list_dedups_and_defaults() {
let list = RouterList::new(["https://r.example/", "https://r.example"]);
assert_eq!(list.list(), vec!["https://r.example".to_string()]);
assert_eq!(
RouterList::new(Vec::<String>::new()).list(),
DEFAULT_ROUTERS
.iter()
.map(|r| r.to_string())
.collect::<Vec<_>>()
);
}
}