use alloc::vec::Vec;
use std::net::{IpAddr, SocketAddr, UdpSocket};
use std::time::Duration;
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 BlockingClient {
config: RoughtimeClientConfig,
}
impl BlockingClient {
#[must_use]
pub const fn new(config: RoughtimeClientConfig) -> Self {
Self { config }
}
pub fn query(
&self,
resolved_servers: &[(RoughtimeServer, IpAddr)],
) -> Result<ConsensusResult, crate::Error> {
let mut times = Vec::with_capacity(resolved_servers.len());
for &(srv, ip) in resolved_servers {
if let Ok(verified) = query_single(ip, srv.port, srv.pubkey_base64, &self.config) {
times.push(verified);
}
}
consensus(times, self.config.max_spread)
}
#[cfg(feature = "dnscrypt")]
pub fn query_well_known(&self) -> Result<ConsensusResult, crate::Error> {
let resolved = crate::resolve::resolve_servers(crate::servers::ROUGHTIME_SERVERS);
self.query(&resolved)
}
}
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").map_err(|_err| crate::Error::NoValidResponses)?;
socket
.set_read_timeout(Some(config.timeout))
.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).is_err() {
std::thread::sleep(Duration::from_millis(300));
continue;
}
match socket.recv_from(&mut buf) {
Ok((n, _src)) => {
received_len = Some(n);
break;
}
Err(_) => {
std::thread::sleep(Duration::from_millis(300));
}
}
}
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 std::thread;
use super::{BlockingClient, RoughtimeClientConfig, RoughtimeServer};
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;
fn respond_once_with_a_valid_time(socket: &std::net::UdpSocket) {
let mut buf = [0u8; 2048];
let (n, src) = socket.recv_from(&mut buf).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).unwrap();
}
#[test]
fn queries_a_loopback_server_successfully() {
let socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
let addr = socket.local_addr().unwrap();
let handle = thread::spawn(move || respond_once_with_a_valid_time(&socket));
let server = RoughtimeServer {
name: "test",
host: "localhost",
port: addr.port(),
pubkey_base64: root_pubkey_base64_static(),
};
let client = BlockingClient::new(RoughtimeClientConfig::default());
let result = client.query(&[(server, addr.ip())]).unwrap();
assert!(result.time.midpoint_micros > 0);
handle.join().unwrap();
}
#[test]
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 = BlockingClient::new(config);
let ip: IpAddr = "127.0.0.1".parse().unwrap();
let err = client.query(&[(server, ip)]).unwrap_err();
assert_eq!(err, crate::Error::NoValidResponses);
}
}