use crate::sync::error::{SyncError, SyncResult};
use std::collections::BTreeSet;
use std::net::ToSocketAddrs;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct SyncPeerAddress {
pub url: String,
pub host: String,
pub port: u16,
pub scheme: SyncPeerScheme,
}
impl SyncPeerAddress {
pub fn parse(peer: &str) -> SyncResult<Self> {
let parsed = parse_peer_seed(peer, None)?;
Ok(Self::from_host_port(
parsed.host,
parsed.port,
parsed.scheme,
))
}
fn from_host_port(host: String, port: u16, scheme: SyncPeerScheme) -> Self {
let scheme_text = scheme.as_str();
let url = if host.contains(':') {
format!("{scheme_text}://[{host}]:{port}")
} else {
format!("{scheme_text}://{host}:{port}")
};
Self {
url,
host,
port,
scheme,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SyncPeerScheme {
Http,
Https,
}
impl SyncPeerScheme {
pub fn as_str(self) -> &'static str {
match self {
SyncPeerScheme::Http => "http",
SyncPeerScheme::Https => "https",
}
}
}
pub fn discover_dns_sync_peers(
seeds: &[String],
default_port: u16,
) -> SyncResult<Vec<SyncPeerAddress>> {
let mut peers = BTreeSet::new();
for seed in seeds {
let parsed = parse_peer_seed(seed, Some(default_port))?;
ensure_resolves(&parsed.host, parsed.port)?;
let _ = peers.insert(SyncPeerAddress::from_host_port(
parsed.host,
parsed.port,
parsed.scheme,
));
}
Ok(peers.into_iter().collect())
}
fn ensure_resolves(host: &str, port: u16) -> SyncResult<()> {
let addrs = (host, port)
.to_socket_addrs()
.map_err(|err| SyncError::DnsResolutionFailed(err.to_string()))?
.collect::<Vec<_>>();
if addrs.is_empty() {
return Err(SyncError::DnsResolutionFailed(
"sync dns seed resolved to no addresses".to_string(),
));
}
Ok(())
}
struct ParsedPeerSeed {
host: String,
port: u16,
scheme: SyncPeerScheme,
}
fn parse_peer_seed(peer: &str, default_port: Option<u16>) -> SyncResult<ParsedPeerSeed> {
let (scheme, address) = split_scheme(peer)?;
if address.is_empty() {
return Err(SyncError::InvalidPeerAddress);
}
if let Some((host, port)) = parse_bracketed_ipv6(address)? {
return Ok(ParsedPeerSeed { host, port, scheme });
}
let (host, port) = parse_plain_host_port(address, default_port)?;
Ok(ParsedPeerSeed { host, port, scheme })
}
fn split_scheme(peer: &str) -> SyncResult<(SyncPeerScheme, &str)> {
let peer = peer.trim().trim_end_matches('/');
if let Some(address) = peer.strip_prefix("http://") {
return Ok((SyncPeerScheme::Http, address));
}
if let Some(address) = peer.strip_prefix("https://") {
return Ok((SyncPeerScheme::Https, address));
}
if peer.contains("://") {
return Err(SyncError::UnsupportedPeerScheme);
}
Ok((SyncPeerScheme::Http, peer))
}
fn parse_bracketed_ipv6(peer: &str) -> SyncResult<Option<(String, u16)>> {
if !peer.starts_with('[') {
return Ok(None);
}
let Some((host, rest)) = peer[1..].split_once(']') else {
return Err(SyncError::InvalidPeerAddress);
};
let Some(port) = rest.strip_prefix(':') else {
return Err(SyncError::InvalidPeerAddress);
};
Ok(Some((host.to_string(), parse_port(port)?)))
}
fn parse_plain_host_port(peer: &str, default_port: Option<u16>) -> SyncResult<(String, u16)> {
let mut parts = peer.rsplitn(2, ':');
let last = parts.next().unwrap_or_default();
let host = parts.next();
match (host, default_port) {
(Some(host), _) if !host.is_empty() => Ok((host.to_string(), parse_port(last)?)),
(None, Some(port)) => Ok((last.to_string(), port)),
_ => Err(SyncError::InvalidPeerAddress),
}
}
fn parse_port(raw: &str) -> SyncResult<u16> {
raw.parse::<u16>()
.map_err(|_| SyncError::InvalidPeerAddress)
}
#[cfg(test)]
#[path = "discovery_tests.rs"]
mod tests;