use std::net::IpAddr;
use serde_json::Value;
use tokio::net::lookup_host;
use crate::{
chaindef::BlockHash,
rpc::parseutil::{hash_from_value, parse_version_str},
};
use super::peer::CandidatePeer;
use anyhow::{Context, Result};
const MAX_CANDIDATES_PER_REQ: usize = 10;
fn parse_port(port: Option<&Value>) -> Option<u16> {
match port.and_then(|p| p.as_u64()) {
Some(p) => {
if p > u16::MAX as u64 {
None
} else {
Some(p as u16)
}
}
None => None,
}
}
pub async fn lookup_hosts(hosts: Option<&Value>, peer_ip: IpAddr) -> Vec<CandidatePeer> {
let hosts = match hosts.and_then(|h| h.as_object()) {
Some(h) => h,
None => return vec![],
};
let mut candidates: Vec<CandidatePeer> = Vec::default();
for (i, (claimed_hostname, ports)) in hosts.into_iter().enumerate() {
if i >= MAX_CANDIDATES_PER_REQ {
break;
}
let tcp_port = parse_port(ports.get("tcp_port"));
let ssl_port = parse_port(ports.get("ssl_port"));
let ws_port = parse_port(ports.get("ws_port"));
let wss_port = parse_port(ports.get("wss_port"));
if tcp_port.is_none() && ssl_port.is_none() && ws_port.is_none() && wss_port.is_none() {
continue;
}
let resolve_test = format!(
"{claimed_hostname}:{}",
tcp_port.unwrap_or_else(|| ssl_port.unwrap())
);
let hostname = match lookup_host(resolve_test).await {
Ok(resolves) => {
if !resolves
.into_iter()
.any(|host_addr| host_addr.ip() == peer_ip)
{
trace!("discover: ignoring hostname '{claimed_hostname}', did not resolve to expected IP '{peer_ip}'");
None
} else {
Some(claimed_hostname)
}
}
Err(e) => {
trace!(
"discover: ignoring hostname '{claimed_hostname}' due to resolve error: {e}"
);
None
}
};
candidates.push(CandidatePeer {
tcp_port,
ssl_port,
ws_port,
wss_port,
hostname: hostname.cloned(),
ip: peer_ip,
version: None,
});
}
candidates
}
pub async fn parse_candidates_from_features(
features: &Value,
peer_ip: IpAddr,
genesis_hash: Option<&BlockHash>,
) -> Result<Vec<CandidatePeer>> {
if let Some(genesis_hash) = genesis_hash {
let peer_genesis: BlockHash = hash_from_value(features.get("genesis_hash"))?;
if peer_genesis != *genesis_hash {
bail!("Genesis mismatch {peer_genesis} != {genesis_hash}");
}
}
let peer_version = features
.get("protocol_max")
.context("No protocol version provided")?
.as_str()
.context("Protocol version not a string")?;
let peer_version = parse_version_str(peer_version).context("invalid version")?;
let candidates = lookup_hosts(features.get("hosts"), peer_ip).await;
Ok(candidates
.into_iter()
.map(|mut c| {
c.version = Some(peer_version.to_string());
c
})
.collect())
}