use roughenough_common::crypto::secure_shuffle;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerList {
servers: Vec<Server>,
#[serde(skip_serializing_if = "Option::is_none")]
sources: Option<Vec<String>>,
#[serde(rename = "reports", skip_serializing_if = "Option::is_none")]
reporting_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Server {
name: String,
version: ServerVersion,
#[serde(rename = "publicKeyType")]
public_key_type: String,
#[serde(rename = "publicKey")]
public_key: String,
addresses: Vec<Address>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ServerVersion {
Integer(u32),
Legacy(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Address {
protocol: Protocol,
address: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Protocol {
Tcp,
Udp,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("field '{field}' is empty")]
EmptyField { field: String },
#[error("Invalid address: expected 'host:port', got '{address}'")]
InvalidAddress { address: String },
#[error("Invalid URL: {reason}")]
InvalidUrl { reason: String },
#[error("JSON error: {0}")]
InvalidJson(#[from] serde_json::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Configuration error: {0}")]
ConfigError(String),
}
impl ServerList {
pub fn new(
servers: Vec<Server>,
sources: Option<Vec<String>>,
reports: Option<String>,
) -> Result<Self, Error> {
let server_list = Self {
servers,
sources,
reporting_url: reports,
};
server_list.validate()?;
Ok(server_list)
}
pub fn validate(&self) -> Result<(), Error> {
if self.servers.is_empty() {
return Err(Error::EmptyField {
field: "server list".to_string(),
});
}
for server in self.servers.iter() {
server.validate()?;
}
if let Some(sources) = &self.sources {
for url in sources.iter() {
if !url.starts_with("https://") {
return Err(Error::InvalidUrl {
reason: format!("Source URL must use HTTPS scheme: {url}"),
});
}
}
}
if let Some(reports) = &self.reporting_url
&& !reports.starts_with("https://")
{
return Err(Error::InvalidUrl {
reason: format!("Reports URL must use HTTPS scheme: {reports}"),
});
}
Ok(())
}
pub fn from_json(json: &str) -> Result<Self, Error> {
let server_list: ServerList = serde_json::from_str(json)?;
server_list.validate()?;
Ok(server_list)
}
pub fn to_json(&self) -> Result<String, Error> {
serde_json::to_string_pretty(self).map_err(Into::into)
}
pub fn from_file(file_name: &str) -> Result<Self, Error> {
let json = std::fs::read_to_string(file_name)?;
Self::from_json(&json)
}
pub fn choose_random(&self, n: usize) -> Result<Vec<Server>, Error> {
if n > self.servers.len() {
return Err(Error::ConfigError(format!(
"requested {} servers but only {} available",
n,
self.servers.len()
)));
}
let mut servers = self.servers.clone();
secure_shuffle(&mut servers);
Ok(servers[..n].to_vec())
}
pub fn servers(&self) -> &[Server] {
&self.servers
}
pub fn reporting_url(&self) -> Option<&str> {
self.reporting_url.as_deref()
}
pub fn add_server(&mut self, server: Server) {
self.servers.push(server);
}
}
impl Server {
pub fn new(
name: String,
version: ServerVersion,
public_key_type: String,
public_key: String,
addresses: Vec<Address>,
) -> Result<Self, Error> {
let server = Self {
name,
version,
public_key_type,
public_key,
addresses,
};
server.validate()?;
Ok(server)
}
pub fn validate(&self) -> Result<(), Error> {
if self.name.is_empty() {
return Err(Error::EmptyField {
field: "name".to_string(),
});
}
if self.version == ServerVersion::Legacy(String::new()) {
return Err(Error::EmptyField {
field: "version".to_string(),
});
}
if self.public_key_type.is_empty() {
return Err(Error::EmptyField {
field: "publicKeyType".to_string(),
});
}
if self.public_key.is_empty() {
return Err(Error::EmptyField {
field: "publicKey".to_string(),
});
}
if self.addresses.is_empty() {
return Err(Error::EmptyField {
field: format!("{}.addresses", self.name.clone()),
});
}
for address in &self.addresses {
address.validate()?;
}
Ok(())
}
pub fn addresses(&self) -> &[Address] {
&self.addresses
}
pub fn first_address(&self) -> &Address {
&self.addresses[0]
}
pub fn first_udp_address(&self) -> Option<&Address> {
self.addresses.iter().find(|a| a.protocol == Protocol::Udp)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &ServerVersion {
&self.version
}
pub fn public_key_type(&self) -> &str {
&self.public_key_type
}
pub fn public_key(&self) -> &str {
&self.public_key
}
}
impl Address {
pub fn new(protocol: Protocol, address: String) -> Result<Self, Error> {
let addr = Self { protocol, address };
addr.validate()?;
Ok(addr)
}
pub fn validate(&self) -> Result<(), Error> {
if self.address.parse::<std::net::SocketAddr>().is_ok() {
return Ok(());
}
match self.address.rsplit_once(':') {
Some((host, port)) if !host.is_empty() && port.parse::<u16>().is_ok() => Ok(()),
_ => Err(Error::InvalidAddress {
address: self.address.clone(),
}),
}
}
pub fn host(&self) -> &str {
match self.address.rsplit_once(':') {
Some((host, _port)) => host.trim_start_matches('[').trim_end_matches(']'),
None => &self.address,
}
}
pub fn port(&self) -> u16 {
self.address
.rsplit_once(':')
.and_then(|(_host, port)| port.parse().ok())
.expect("was validated at construction")
}
pub fn protocol(&self) -> &Protocol {
&self.protocol
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_list_creation() {
let address =
Address::new(Protocol::Udp, "roughtime.example.com:2002".to_string()).unwrap();
let server = Server::new(
"Example Server".to_string(),
ServerVersion::Integer(1),
"ed25519".to_string(),
"base64encodedkey==".to_string(),
vec![address],
)
.unwrap();
let mut server_list = ServerList::new(vec![server], None, None).unwrap();
assert_eq!(server_list.servers.len(), 1);
let address2 = Address::new(Protocol::Tcp, "other.example.com:2003".to_string()).unwrap();
let server2 = Server::new(
"Other Server".to_string(),
ServerVersion::Integer(1),
"ed25519".to_string(),
"anotherkeyhere==".to_string(),
vec![address2],
)
.unwrap();
server_list.add_server(server2);
assert_eq!(server_list.servers.len(), 2);
}
#[test]
fn test_empty_server_list_fails() {
match ServerList::new(vec![], None, None) {
Err(Error::EmptyField { field }) => assert_eq!(field, "server list"),
Err(e) => panic!("expected Error::EmptyField, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
}
#[test]
fn test_address_parsing() {
let address = Address::new(Protocol::Udp, "example.com:2002".to_string()).unwrap();
assert_eq!(address.host(), "example.com");
assert_eq!(address.port(), 2002);
}
#[test]
fn test_invalid_addresses() {
match Address::new(Protocol::Udp, "invalid_address".to_string()) {
Err(Error::InvalidAddress { address }) => assert_eq!(address, "invalid_address"),
Err(e) => panic!("expected Error::InvalidAddress, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
match Address::new(Protocol::Udp, "example.com:not_a_port".to_string()) {
Err(Error::InvalidAddress { address }) => assert_eq!(address, "example.com:not_a_port"),
Err(e) => panic!("expected Error::InvalidAddress, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
}
#[test]
fn test_ipv6_address_parses() {
let json = r#"{
"servers": [{
"name": "V6 Server",
"version": 1,
"publicKeyType": "ed25519",
"publicKey": "key==",
"addresses": [{"protocol": "udp", "address": "[2001:db8::1]:2003"}]
}]
}"#;
let server_list = ServerList::from_json(json).unwrap();
let address = server_list.servers()[0].first_address();
assert_eq!(address.host(), "2001:db8::1");
assert_eq!(address.port(), 2003);
}
#[test]
fn test_ipv4_and_hostname_addresses_still_parse() {
let v4 = Address::new(Protocol::Udp, "192.0.2.7:2003".to_string()).unwrap();
assert_eq!(v4.host(), "192.0.2.7");
assert_eq!(v4.port(), 2003);
let hostname =
Address::new(Protocol::Udp, "roughtime.example.com:2002".to_string()).unwrap();
assert_eq!(hostname.host(), "roughtime.example.com");
assert_eq!(hostname.port(), 2002);
}
#[test]
fn test_tcp_only_server_has_no_udp_address() {
let tcp = Address::new(Protocol::Tcp, "example.com:2003".to_string()).unwrap();
let server = Server::new(
"TCP Only".to_string(),
ServerVersion::Integer(1),
"ed25519".to_string(),
"key==".to_string(),
vec![tcp],
)
.unwrap();
assert!(server.first_udp_address().is_none());
}
#[test]
fn test_mixed_addresses_select_udp() {
let tcp = Address::new(Protocol::Tcp, "example.com:2003".to_string()).unwrap();
let udp = Address::new(Protocol::Udp, "example.com:2004".to_string()).unwrap();
let server = Server::new(
"Mixed".to_string(),
ServerVersion::Integer(1),
"ed25519".to_string(),
"key==".to_string(),
vec![tcp, udp],
)
.unwrap();
let selected = server.first_udp_address().unwrap();
assert_eq!(*selected.protocol(), Protocol::Udp);
assert_eq!(selected.port(), 2004);
}
#[test]
fn test_json_serialization_roundtrip() {
let address =
Address::new(Protocol::Udp, "roughtime.example.com:2002".to_string()).unwrap();
let server = Server::new(
"Example Server".to_string(),
ServerVersion::Integer(1),
"ed25519".to_string(),
"base64encodedkey==".to_string(),
vec![address],
)
.unwrap();
let servers = vec![server];
let server_list = ServerList::new(servers, None, None).unwrap();
let json = server_list.to_json().unwrap();
let parsed: ServerList = ServerList::from_json(&json).unwrap();
assert_eq!(parsed.servers.len(), 1);
assert_eq!(parsed.servers[0].name, "Example Server");
}
#[test]
fn integer_and_legacy_versions_parse() {
let json = r#"{
"servers": [
{
"name": "RFC",
"version": 1,
"publicKeyType": "ed25519",
"publicKey": "key==",
"addresses": [{"protocol": "udp", "address": "example.com:5319"}]
},
{
"name": "Legacy",
"version": "IETF-Roughtime",
"publicKeyType": "ed25519",
"publicKey": "key==",
"addresses": [{"protocol": "udp", "address": "example.com:2002"}]
}
]
}"#;
let server_list = ServerList::from_json(json).unwrap();
assert_eq!(
server_list.servers()[0].version(),
&ServerVersion::Integer(1)
);
assert_eq!(
server_list.servers()[1].version(),
&ServerVersion::Legacy("IETF-Roughtime".to_string())
);
let json = server_list.to_json().unwrap();
assert!(json.contains(r#""version": 1"#) || json.contains(r#""version":1"#));
}
#[test]
fn empty_legacy_version_is_rejected() {
let json = r#"{
"servers": [{
"name": "Server",
"version": "",
"publicKeyType": "ed25519",
"publicKey": "key==",
"addresses": [{"protocol": "udp", "address": "example.com:5319"}]
}]
}"#;
match ServerList::from_json(json) {
Err(Error::EmptyField { field }) => assert_eq!(field, "version"),
Err(e) => panic!("expected Error::EmptyField, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
}
#[test]
fn can_read_cloudflare_list() {
let json = include_str!("../testdata/serverlist-cloudflare.json");
let server_list: ServerList = ServerList::from_json(json).unwrap();
assert_eq!(server_list.servers.len(), 4);
}
#[test]
fn test_invalid_json_fails() {
let json = r#"{"servers": []}"#;
match ServerList::from_json(json) {
Err(Error::EmptyField { field }) => assert_eq!(field, "server list"),
Err(e) => panic!("expected Error::EmptyField, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
let json = r#"{
"servers": [{
"name": "",
"version": 1,
"publicKeyType": "ed25519",
"publicKey": "key==",
"addresses": [{"protocol": "udp", "address": "example.com:2002"}]
}]
}"#;
match ServerList::from_json(json) {
Err(Error::EmptyField { field }) => assert_eq!(field, "name"),
Err(e) => panic!("expected Error::EmptyField, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
let json = r#"{
"servers": [{
"name": "Server",
"version": 1,
"publicKeyType": "ed25519",
"publicKey": "key==",
"addresses": [{"protocol": "udp", "address": "no_port"}]
}]
}"#;
match ServerList::from_json(json) {
Err(Error::InvalidAddress { address }) => assert_eq!(address, "no_port"),
Err(e) => panic!("expected Error::InvalidAddress, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
let json = r#"{
"servers": [{
"name": "Server",
"version": 1,
"publicKeyType": "ed25519",
"publicKey": "key==",
"addresses": [{"protocol": "udp", "address": "example.com:2002"}]
}],
"sources": ["http://example.com"]
}"#;
match ServerList::from_json(json) {
Err(Error::InvalidUrl { reason }) => assert!(reason.contains("HTTPS scheme")),
Err(e) => panic!("expected Error::InvalidUrl, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
}
#[test]
fn test_server_validation() {
match Server::new(
"".to_string(),
ServerVersion::Integer(1),
"ed25519".to_string(),
"key==".to_string(),
vec![Address::new(Protocol::Udp, "example.com:2002".to_string()).unwrap()],
) {
Err(Error::EmptyField { field }) => assert_eq!(field, "name"),
Err(e) => panic!("expected Error::EmptyField, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
match Server::new(
"Server".to_string(),
ServerVersion::Integer(1),
"ed25519".to_string(),
"key==".to_string(),
vec![],
) {
Err(Error::EmptyField { field }) => assert!(field.contains("addresses")),
Err(e) => panic!("expected Error::EmptyField, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
}
#[test]
fn test_sources_validation() {
let address = Address::new(Protocol::Udp, "example.com:2002".to_string()).unwrap();
let server = Server::new(
"Server".to_string(),
ServerVersion::Integer(1),
"ed25519".to_string(),
"key==".to_string(),
vec![address],
)
.unwrap();
let result = ServerList::new(
vec![server.clone()],
Some(vec!["https://example.com/list.json".to_string()]),
None,
);
assert!(result.is_ok());
match ServerList::new(
vec![server.clone()],
Some(vec!["http://example.com/list.json".to_string()]),
None,
) {
Err(Error::InvalidUrl { reason }) => assert!(reason.contains("HTTPS scheme")),
Err(e) => panic!("expected Error::InvalidUrl, got {e:?}"),
Ok(_) => panic!("expected validation to fail"),
}
}
}