use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
use roughenough_common::crypto::{make_srv_commitment, random_bytes};
use roughenough_common::encoding::try_decode_key;
use roughenough_protocol::cursor::ParseCursor;
use roughenough_protocol::request::{MAX_RESPONSE_SIZE, Request};
use roughenough_protocol::response::Response;
use roughenough_protocol::tags::{Nonce, ProtocolVersion, PublicKey, SrvCommitment};
use roughenough_protocol::{FromFrame, ToFrame};
use crate::measurement::Measurement;
use crate::transport::{ClientTransport, UdpTransport};
use crate::{ResponseValidator, validation};
pub fn query(hostname: &str, port: u16, key: Option<&str>) -> Result<Measurement, ClientError> {
let client = Client::new(hostname, port, key)?;
client.query()
}
#[derive(thiserror::Error, Debug)]
pub enum ClientError {
#[error("timeout waiting for server response")]
ServerTimeout,
#[error("bad server response: {0}")]
BadResponse(#[from] roughenough_protocol::error::Error),
#[error("public key decode failed: {0}")]
BadPublicKey(#[from] data_encoding::DecodeError),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("validation of the server's response failed: {0}")]
ValidationFailed(#[from] validation::ValidationError),
#[error("invalid configuration: {0}")]
InvalidConfiguration(String),
#[error("could not find IP address for '{0}'")]
DnsLookupFailed(String),
}
pub struct ClientBuilder {
server: SocketAddr,
hostname: String,
transport: Option<Box<dyn ClientTransport>>,
timeout: Option<Duration>,
public_key: Option<PublicKey>,
versions: Option<Vec<ProtocolVersion>>,
}
impl ClientBuilder {
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
pub fn new(server: SocketAddr) -> Self {
Self {
server,
hostname: "unknown".to_string(),
transport: None,
timeout: None,
public_key: None,
versions: None,
}
}
pub fn hostname(mut self, hostname: &str) -> Self {
self.hostname = hostname.to_string();
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn transport(mut self, transport: Box<dyn ClientTransport>) -> Self {
self.transport = Some(transport);
self
}
pub fn public_key(mut self, public_key: PublicKey) -> Self {
self.public_key = Some(public_key);
self
}
pub fn versions(mut self, versions: &[ProtocolVersion]) -> Self {
assert!(!versions.is_empty(), "versions must be non-empty");
self.versions = Some(versions.to_vec());
self
}
pub fn build(self) -> Client {
let timeout = self.timeout.unwrap_or(Self::DEFAULT_TIMEOUT);
let transport = self
.transport
.unwrap_or_else(|| Box::new(UdpTransport::new(&self.server, timeout)));
let srv_commit = self
.public_key
.map(|public_key| make_srv_commitment(&public_key));
let validator = match self.public_key {
Some(public_key) => ResponseValidator::new_with_key(public_key),
None => ResponseValidator::new(),
};
Client {
transport,
validator,
srv_commit,
server: self.server,
hostname: self.hostname,
public_key: self.public_key,
versions: self.versions,
}
}
}
pub struct Client {
pub(crate) server: SocketAddr,
pub(crate) hostname: String,
pub(crate) transport: Box<dyn ClientTransport>,
pub(crate) validator: ResponseValidator,
pub(crate) public_key: Option<PublicKey>,
pub(crate) srv_commit: Option<SrvCommitment>,
pub(crate) versions: Option<Vec<ProtocolVersion>>,
}
impl Client {
pub fn builder(server: SocketAddr) -> ClientBuilder {
ClientBuilder::new(server)
}
pub fn new(
hostname: &str,
port: u16,
pub_key: Option<impl AsRef<str>>,
) -> Result<Self, ClientError> {
Self::new_with_versions(hostname, port, pub_key, None)
}
pub fn new_with_versions(
hostname: &str,
port: u16,
pub_key: Option<impl AsRef<str>>,
versions: Option<&[ProtocolVersion]>,
) -> Result<Self, ClientError> {
let host_port = format!("{hostname}:{port}");
let sock_addr = host_port
.to_socket_addrs()?
.next()
.ok_or(ClientError::DnsLookupFailed(host_port))?;
let mut builder = Self::builder(sock_addr).hostname(hostname);
if let Some(encoded_key) = pub_key {
let pub_key = try_decode_key(encoded_key.as_ref())?;
builder = builder.public_key(pub_key);
}
if let Some(versions) = versions {
builder = builder.versions(versions);
}
Ok(builder.build())
}
pub fn query(&self) -> Result<Measurement, ClientError> {
let request = self.create_request(None);
let nbytes = self.send_request(&request)?;
assert_eq!(nbytes, request.frame_size());
let (response, response_bytes) = self.recv_response()?;
let request_bytes = request.as_frame_bytes()?;
let _midpoint = self
.validator
.validate(&request_bytes, &response_bytes, &response)?;
Measurement::builder()
.server(self.server)
.hostname(self.hostname.clone())
.public_key(self.public_key)
.request(request)
.response(response)
.response_bytes(response_bytes)
.rand_value(None)
.build()
}
fn create_request(&self, nonce: Option<Nonce>) -> Request {
let nonce = nonce.unwrap_or_else(|| Nonce::from(random_bytes::<32>()));
match (&self.srv_commit, &self.versions) {
(Some(srv), Some(versions)) => {
Request::new_with_server_and_versions(&nonce, srv, versions)
}
(Some(srv), None) => Request::new_with_server(&nonce, srv),
(None, Some(versions)) => Request::new_with_versions(&nonce, versions),
(None, None) => Request::new(&nonce),
}
}
fn send_request(&self, request: &Request) -> Result<usize, ClientError> {
let request_bytes = request.as_frame_bytes()?;
self.transport.send(&request_bytes, self.server)
}
fn recv_response(&self) -> Result<(Response, Vec<u8>), ClientError> {
let mut buf = [0u8; MAX_RESPONSE_SIZE];
let (nbytes, _addr) = self.transport.recv(&mut buf)?;
let response_bytes = buf[..nbytes].to_vec();
let mut cursor = ParseCursor::new(&mut buf[..nbytes]);
let response = Response::from_frame(&mut cursor)?;
Ok((response, response_bytes))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_addr() -> SocketAddr {
"127.0.0.1:2003".parse().unwrap()
}
fn test_key() -> PublicKey {
PublicKey::from([0x42u8; 32])
}
#[test]
fn builder_defaults() {
let client = Client::builder(test_addr()).build();
assert_eq!(client.server, test_addr());
assert_eq!(client.hostname, "unknown");
assert!(client.public_key.is_none());
assert!(client.srv_commit.is_none());
assert!(client.versions.is_none());
}
#[test]
fn builder_with_key_derives_srv_commitment() {
let key = test_key();
let client = Client::builder(test_addr()).public_key(key).build();
assert_eq!(client.public_key, Some(key));
assert_eq!(client.srv_commit, Some(make_srv_commitment(&key)));
}
#[test]
fn request_without_key_omits_srv() {
let client = Client::builder(test_addr()).build();
let request = client.create_request(None);
assert!(request.srv().is_none());
}
#[test]
fn request_with_key_carries_srv_commitment() {
let key = test_key();
let client = Client::builder(test_addr()).public_key(key).build();
let request = client.create_request(None);
assert_eq!(request.srv(), Some(&make_srv_commitment(&key)));
}
#[test]
fn request_carries_configured_versions() {
let versions = [ProtocolVersion::RFC];
let client = Client::builder(test_addr()).versions(&versions).build();
let request = client.create_request(None);
assert_eq!(request.ver().versions(), &versions[..]);
}
#[test]
fn request_uses_provided_nonce() {
let client = Client::builder(test_addr()).build();
let nonce = Nonce::from([0x77u8; 32]);
let request = client.create_request(Some(nonce));
assert_eq!(request.nonc(), &nonce);
}
#[test]
fn query_surfaces_transport_timeout() {
struct DeafTransport;
impl ClientTransport for DeafTransport {
fn send(&self, data: &[u8], _addr: SocketAddr) -> Result<usize, ClientError> {
Ok(data.len())
}
fn recv(&self, _buf: &mut [u8]) -> Result<(usize, SocketAddr), ClientError> {
Err(ClientError::ServerTimeout)
}
}
let client = Client::builder(test_addr())
.transport(Box::new(DeafTransport))
.build();
match client.query() {
Err(ClientError::ServerTimeout) => {}
other => panic!("expected ServerTimeout, got {other:?}"),
}
}
}