use std::borrow::Cow;
use std::net::{IpAddr, Ipv4Addr};
use js_sys::Promise;
use libp2p::multiaddr::Protocol;
use libp2p::{Multiaddr, PeerId};
use serde::Deserialize;
use tracing::warn;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
const DEFAULT_DNS_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
#[derive(thiserror::Error, Debug)]
pub(crate) enum Error {
#[error("Failed fetching: {0}")]
CouldNotFetch(String),
#[error("Could not parse response: {0}")]
CouldNotParseResponse(String),
}
pub(crate) async fn resolve_bootnode_addresses(addrs: Vec<Multiaddr>) -> Vec<Multiaddr> {
let mut bootnodes = Vec::with_capacity(addrs.len());
for addr in addrs {
match resolve_dnsaddr_multiaddress(&addr, DEFAULT_DNS_ADDR).await {
Ok(resolved_addrs) => bootnodes.extend(resolved_addrs.into_iter()),
Err(e) => warn!("Failed to resolve {addr}: {e}"),
}
}
bootnodes
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = fetch)]
fn fetch_with_request(input: &Request) -> Promise;
}
async fn fetch(url: &str, opts: &RequestInit, headers: &[(&str, &str)]) -> Result<Response, Error> {
let request = Request::new_with_str_and_init(url, opts)
.map_err(|e| Error::CouldNotFetch(format!("failed to prepare request: {e:?}")))?;
for (name, value) in headers {
request.headers().set(name, value).map_err(|_| {
Error::CouldNotFetch(format!("failed setting header: '{name}: {value}'"))
})?;
}
let fetch_promise = fetch_with_request(&request);
JsFuture::from(fetch_promise)
.await
.map_err(|e| Error::CouldNotFetch(format!("failed fetching {url}: {e:?}")))?
.dyn_into()
.map_err(|_| Error::CouldNotFetch("`response` is not `Response` type".to_string()))
}
fn get_peer_id(ma: &Multiaddr) -> Option<PeerId> {
ma.iter().find_map(|protocol| {
if let Protocol::P2p(peer_id) = protocol {
Some(peer_id)
} else {
None
}
})
}
fn get_dnsaddr(ma: &Multiaddr) -> Option<Cow<'_, str>> {
ma.iter().find_map(|protocol| {
if let Protocol::Dnsaddr(addr) = protocol {
Some(addr)
} else {
None
}
})
}
pub async fn resolve_dnsaddr_multiaddress(
ma: &Multiaddr,
dns_ip: IpAddr,
) -> Result<Vec<Multiaddr>, Error> {
const TXT_TYPE: u16 = 16;
#[derive(Debug, Deserialize)]
struct DohEntry {
r#type: u16,
data: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct DohResponse {
answer: Vec<DohEntry>,
}
let Some(dnsaddr) = get_dnsaddr(ma) else {
return Ok(vec![ma.to_owned()]);
};
let Some(peer_id) = get_peer_id(ma) else {
return Err(Error::CouldNotFetch(
"failed preparing request: PeerId missing".to_string(),
));
};
let opts = RequestInit::new();
opts.set_method("GET");
opts.set_mode(RequestMode::Cors);
let url = format!("https://{dns_ip}/dns-query?type={TXT_TYPE}&name=_dnsaddr.{dnsaddr}");
let response = fetch(&url, &opts, &[("Accept", "application/dns-json")]).await?;
let json_promise = response
.json()
.map_err(|e| Error::CouldNotParseResponse(format!("Response::json() failed: {e:?}")))?;
let json = JsFuture::from(json_promise).await.map_err(|e| {
Error::CouldNotParseResponse(format!("Failed parsing response as json: {e:?}"))
})?;
let doh_response: DohResponse = serde_wasm_bindgen::from_value(json).map_err(|e| {
Error::CouldNotParseResponse(format!("Failed deserializing DoH response: {e}"))
})?;
let mut resolved_addrs = Vec::with_capacity(3);
for entry in doh_response.answer {
if entry.r#type == TXT_TYPE {
let Ok(data) = serde_json::from_str::<String>(&entry.data) else {
continue;
};
let Some((_, ma)) = data.split_once('=') else {
continue;
};
let Ok(ma) = ma.parse() else {
continue;
};
if Some(peer_id) == get_peer_id(&ma) {
resolved_addrs.push(ma);
}
}
}
Ok(resolved_addrs)
}