appcore_sync/sync/
discovery.rs1use crate::sync::error::{SyncError, SyncResult};
14use std::collections::BTreeSet;
15use std::net::ToSocketAddrs;
16
17#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
18pub struct SyncPeerAddress {
20 pub url: String,
22 pub host: String,
24 pub port: u16,
26 pub scheme: SyncPeerScheme,
28}
29
30impl SyncPeerAddress {
31 pub fn parse(peer: &str) -> SyncResult<Self> {
33 let parsed = parse_peer_seed(peer, None)?;
34 Ok(Self::from_host_port(
35 parsed.host,
36 parsed.port,
37 parsed.scheme,
38 ))
39 }
40
41 fn from_host_port(host: String, port: u16, scheme: SyncPeerScheme) -> Self {
42 let scheme_text = scheme.as_str();
43 let url = if host.contains(':') {
44 format!("{scheme_text}://[{host}]:{port}")
45 } else {
46 format!("{scheme_text}://{host}:{port}")
47 };
48 Self {
49 url,
50 host,
51 port,
52 scheme,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
58pub enum SyncPeerScheme {
60 Http,
62 Https,
64}
65
66impl SyncPeerScheme {
67 pub fn as_str(self) -> &'static str {
69 match self {
70 SyncPeerScheme::Http => "http",
71 SyncPeerScheme::Https => "https",
72 }
73 }
74}
75
76pub fn discover_dns_sync_peers(
78 seeds: &[String],
79 default_port: u16,
80) -> SyncResult<Vec<SyncPeerAddress>> {
81 let mut peers = BTreeSet::new();
82 for seed in seeds {
83 let parsed = parse_peer_seed(seed, Some(default_port))?;
84 ensure_resolves(&parsed.host, parsed.port)?;
85 let _ = peers.insert(SyncPeerAddress::from_host_port(
86 parsed.host,
87 parsed.port,
88 parsed.scheme,
89 ));
90 }
91 Ok(peers.into_iter().collect())
92}
93
94fn ensure_resolves(host: &str, port: u16) -> SyncResult<()> {
95 let addrs = (host, port)
96 .to_socket_addrs()
97 .map_err(|err| SyncError::DnsResolutionFailed(err.to_string()))?
98 .collect::<Vec<_>>();
99 if addrs.is_empty() {
100 return Err(SyncError::DnsResolutionFailed(
101 "sync dns seed resolved to no addresses".to_string(),
102 ));
103 }
104 Ok(())
105}
106
107struct ParsedPeerSeed {
108 host: String,
109 port: u16,
110 scheme: SyncPeerScheme,
111}
112
113fn parse_peer_seed(peer: &str, default_port: Option<u16>) -> SyncResult<ParsedPeerSeed> {
114 let (scheme, address) = split_scheme(peer)?;
115 if address.is_empty() {
116 return Err(SyncError::InvalidPeerAddress);
117 }
118 if let Some((host, port)) = parse_bracketed_ipv6(address)? {
119 return Ok(ParsedPeerSeed { host, port, scheme });
120 }
121 let (host, port) = parse_plain_host_port(address, default_port)?;
122 Ok(ParsedPeerSeed { host, port, scheme })
123}
124
125fn split_scheme(peer: &str) -> SyncResult<(SyncPeerScheme, &str)> {
126 let peer = peer.trim().trim_end_matches('/');
127 if let Some(address) = peer.strip_prefix("http://") {
128 return Ok((SyncPeerScheme::Http, address));
129 }
130 if let Some(address) = peer.strip_prefix("https://") {
131 return Ok((SyncPeerScheme::Https, address));
132 }
133 if peer.contains("://") {
134 return Err(SyncError::UnsupportedPeerScheme);
135 }
136 Ok((SyncPeerScheme::Http, peer))
137}
138
139fn parse_bracketed_ipv6(peer: &str) -> SyncResult<Option<(String, u16)>> {
140 if !peer.starts_with('[') {
141 return Ok(None);
142 }
143 let Some((host, rest)) = peer[1..].split_once(']') else {
144 return Err(SyncError::InvalidPeerAddress);
145 };
146 let Some(port) = rest.strip_prefix(':') else {
147 return Err(SyncError::InvalidPeerAddress);
148 };
149 Ok(Some((host.to_string(), parse_port(port)?)))
150}
151
152fn parse_plain_host_port(peer: &str, default_port: Option<u16>) -> SyncResult<(String, u16)> {
153 let mut parts = peer.rsplitn(2, ':');
154 let last = parts.next().unwrap_or_default();
155 let host = parts.next();
156 match (host, default_port) {
157 (Some(host), _) if !host.is_empty() => Ok((host.to_string(), parse_port(last)?)),
158 (None, Some(port)) => Ok((last.to_string(), port)),
159 _ => Err(SyncError::InvalidPeerAddress),
160 }
161}
162
163fn parse_port(raw: &str) -> SyncResult<u16> {
164 raw.parse::<u16>()
165 .map_err(|_| SyncError::InvalidPeerAddress)
166}
167
168#[cfg(test)]
169#[path = "discovery_tests.rs"]
170mod tests;