use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use url::{Host, Url};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ListedHost {
#[serde(skip_serializing_if = "Option::is_none")]
pub account_count: Option<i64>,
pub hostname: SmolStr,
#[serde(skip_serializing_if = "Option::is_none")]
pub seq: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<SmolStr>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ListHostsOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<SmolStr>,
pub hosts: Vec<ListedHost>,
}
#[cfg(feature = "indexer")]
pub(crate) use crate::net::is_private_ip;
pub(crate) use crate::net::{HostError as PdsHostError, PublicHost as CanonicalPdsHost};
pub(crate) fn canonical_pds_host(input: &str) -> Result<CanonicalPdsHost, PdsHostError> {
crate::net::PublicHost::parse(input)
}
/// validate and normalize the canonical direct-PDS websocket source URL.
///
/// This remains the storage/admission canonicalization law. Public HTTP
/// endpoints use [`crate::net::PublicEndpoint`] instead because XRPC paths and
/// non-default ports are meaningful there.
pub(crate) fn canonical_pds_url(url: &Url) -> Result<(CanonicalPdsHost, Url), PdsHostError> {
if url.scheme() != "wss"
|| !url.username().is_empty()
|| url.password().is_some()
|| url.port().is_some()
|| url.path() != "/"
|| url.query().is_some()
|| url.fragment().is_some()
{
return Err(PdsHostError::NonCanonicalUrl);
}
let host = match url.host().ok_or(PdsHostError::NonCanonicalUrl)? {
Host::Domain(host) => canonical_pds_host(host)?,
Host::Ipv4(ip) => canonical_pds_host(&ip.to_string())?,
Host::Ipv6(ip) => canonical_pds_host(&ip.to_string())?,
};
let normalized = host.firehose_url();
Ok((host, normalized))
}
pub(crate) fn list_hosts_url(base: &Url, cursor: Option<&str>) -> Url {
let mut url = base.clone();
url.set_path("/xrpc/com.atproto.sync.listHosts");
{
let mut pairs = url.query_pairs_mut();
pairs.append_pair("limit", "1000");
if let Some(cursor) = cursor {
pairs.append_pair("cursor", cursor);
}
}
url
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonicalizes_public_dns_hosts_and_rejects_private_hosts() {
let host = canonical_pds_host("BÜCHER.Example.").unwrap();
assert_eq!(host.as_str(), "xn--bcher-kva.example");
assert_eq!(host.firehose_url().as_str(), "wss://xn--bcher-kva.example/");
assert_eq!(
canonical_pds_host("127.0.0.1"),
Err(PdsHostError::NonPublicIp)
);
assert_eq!(
canonical_pds_url(&Url::parse("ws://pds.example/").unwrap()),
Err(PdsHostError::NonCanonicalUrl)
);
}
}