use alloc::vec::Vec;
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use tokio::net::UdpSocket;
use tokio::time::timeout;
use crate::request::{Nonce, build_request};
use crate::servers::RoughtimeServer;
use crate::verify::verify_response;
use super::{ConsensusResult, RoughtimeClientConfig, consensus};
#[derive(Debug, Clone, Copy)]
pub struct TokioClient {
config: RoughtimeClientConfig,
}
impl TokioClient {
#[must_use]
pub const fn new(config: RoughtimeClientConfig) -> Self {
Self { config }
}
pub async fn query(
&self,
resolved_servers: &[(RoughtimeServer, IpAddr)],
) -> Result<ConsensusResult, crate::Error> {
let mut handles = Vec::with_capacity(resolved_servers.len());
for &(srv, ip) in resolved_servers {
let config = self.config;
handles.push(tokio::spawn(async move {
query_single(ip, srv.port, srv.pubkey_base64, &config).await
}));
}
let mut times = Vec::with_capacity(handles.len());
for handle in handles {
if let Ok(Ok(verified)) = handle.await {
times.push(verified);
}
}
consensus(times, self.config.max_spread)
}
#[cfg(feature = "dnscrypt")]
pub async fn query_well_known(&self) -> Result<ConsensusResult, crate::Error> {
let resolved = crate::resolve::resolve_servers_async(crate::servers::ROUGHTIME_SERVERS).await;
self.query(&resolved).await
}
}
async fn query_single(
ip: IpAddr,
port: u16,
pubkey_base64: &str,
config: &RoughtimeClientConfig,
) -> Result<crate::VerifiedTime, crate::Error> {
let socket = UdpSocket::bind("0.0.0.0:0")
.await
.map_err(|_err| crate::Error::NoValidResponses)?;
let server_addr = SocketAddr::new(ip, port);
let nonce = Nonce::random()?;
let req = build_request(&nonce);
let mut buf = [0u8; 2048];
let mut received_len = None;
for _attempt in 0..config.retries.max(1) {
if socket.send_to(&req, server_addr).await.is_err() {
tokio::time::sleep(Duration::from_millis(300)).await;
continue;
}
match timeout(config.timeout, socket.recv_from(&mut buf)).await {
Ok(Ok((n, _src))) => {
received_len = Some(n);
break;
}
Ok(Err(_)) | Err(_) => {
tokio::time::sleep(Duration::from_millis(300)).await;
}
}
}
let n = received_len.ok_or(crate::Error::NoValidResponses)?;
verify_response(&buf[..n], &nonce, pubkey_base64)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use std::net::IpAddr;
use super::{RoughtimeClientConfig, RoughtimeServer, TokioClient};
use crate::floor::EFFECTIVE_FLOOR_SECS;
use crate::request::Nonce;
use crate::tags::TAG_NONC;
use crate::test_fixtures::{FixtureParams, build_signed_response, root_pubkey_base64_static};
use crate::wire::RtMessage;
#[tokio::test]
async fn queries_a_loopback_server_successfully() {
let socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
let addr = socket.local_addr().unwrap();
let server_task = tokio::spawn(async move {
let mut buf = [0u8; 2048];
let (n, src) = socket.recv_from(&mut buf).await.unwrap();
let nonce_bytes: [u8; 64] = RtMessage::parse(&buf[..n])
.unwrap()
.get(TAG_NONC)
.unwrap()
.try_into()
.unwrap();
let mut params = FixtureParams::valid(0, (EFFECTIVE_FLOOR_SECS + 10) * 1_000_000);
params.nonce = Nonce::new(nonce_bytes);
let signed = build_signed_response(¶ms);
socket.send_to(&signed.bytes, src).await.unwrap();
});
let server = RoughtimeServer {
name: "test",
host: "localhost",
port: addr.port(),
pubkey_base64: root_pubkey_base64_static(),
};
let client = TokioClient::new(RoughtimeClientConfig::default());
let result = client.query(&[(server, addr.ip())]).await.unwrap();
assert!(result.time.midpoint_micros > 0);
server_task.await.unwrap();
}
#[tokio::test]
async fn an_unreachable_server_yields_no_valid_responses() {
let config = RoughtimeClientConfig {
timeout: std::time::Duration::from_millis(100),
retries: 1,
..RoughtimeClientConfig::default()
};
let server = RoughtimeServer {
name: "test",
host: "localhost",
port: 1, pubkey_base64: root_pubkey_base64_static(),
};
let client = TokioClient::new(config);
let ip: IpAddr = "127.0.0.1".parse().unwrap();
let err = client.query(&[(server, ip)]).await.unwrap_err();
assert_eq!(err, crate::Error::NoValidResponses);
}
}