rostrum 14.0.1

An efficient implementation of Electrum Server with token support
Documentation
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 {
                // invalid port
                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() {
            // bad host; no known protocols
            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) => {
                // Verify that the hostname resolves to the IP address the peer connected from,
                // otherwise ignore hostname. Could be a working server with a misconfigured hostname.
                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
            }
        };

        // TODO: Try reverse lookup on IP if we did not get a hostname

        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())
}