use crate::boundary::reply::Reply;
use crate::wire::frame;
use crate::wire::material::Material;
use rustls::pki_types::ServerName;
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use serde_json::Value;
use std::net::{IpAddr, TcpStream};
use std::sync::Arc;
use std::time::Duration;
pub(crate) type Landed = Result<Reply, String>;
const ASK_TIMEOUT: Duration = Duration::from_mins(2);
pub(crate) struct Seat {
config: Arc<ClientConfig>,
address: String,
name: ServerName<'static>,
}
impl Seat {
pub(crate) fn open(m: &Material) -> Result<Self, String> {
Ok(Self {
config: crate::wire::tls::client_config(m)?,
address: m.address.clone(),
name: server_name(&m.address)?,
})
}
pub(crate) fn address(&self) -> String {
self.address.clone()
}
pub(crate) fn ask(&self, request: &Value) -> Result<Vec<Value>, String> {
let mut tls = self.dial(request)?;
let mut stream = Vec::new();
loop {
match frame::read_value(&mut tls).map_err(|e| format!("receive: {e}"))? {
Some(chunk) => stream.push(chunk),
None => return Ok(stream),
}
}
}
pub(crate) fn followed(
&self,
request: &Value,
on_frame: &mut dyn FnMut(Landed) -> bool,
) -> Result<(), String> {
let mut tls = self.dial(request)?;
while let Some(chunk) = frame::read_value(&mut tls).map_err(|e| format!("receive: {e}"))? {
if !on_frame(crate::boundary::reply::decode(&chunk).unwrap_or_else(Err)) {
return Ok(());
}
}
Ok(())
}
fn dial(&self, request: &Value) -> Result<StreamOwned<ClientConnection, TcpStream>, String> {
let tcp = TcpStream::connect(&self.address)
.map_err(|e| format!("connect {}: {e}", self.address))?;
tcp.set_read_timeout(Some(ASK_TIMEOUT))
.map_err(|e| format!("connect {}: {e}", self.address))?;
let conn = ClientConnection::new(Arc::clone(&self.config), self.name.clone())
.map_err(|e| format!("tls {}: {e}", self.address))?;
let mut tls = StreamOwned::new(conn, tcp);
crate::wire::hello::state(&mut tls).map_err(|e| format!("send: {e}"))?;
frame::write_value(&mut tls, request).map_err(|e| format!("send: {e}"))?;
crate::wire::hello::confirm(&mut tls)?;
Ok(tls)
}
}
fn server_name(address: &str) -> Result<ServerName<'static>, String> {
let host = address.rsplit_once(':').map_or(address, |(head, _)| head);
let host = host.trim_start_matches('[').trim_end_matches(']');
if let Ok(ip) = host.parse::<IpAddr>() {
return Ok(ServerName::IpAddress(ip.into()));
}
ServerName::try_from(host.to_owned()).map_err(|e| format!("{address}: not a server name: {e}"))
}
pub(crate) fn loopback(bound: &str) -> String {
let port = bound.rsplit_once(':').map_or("", |(_, port)| port);
format!("{}:{port}", crate::wire::provision::LOOPBACK)
}
#[cfg(test)]
mod tests;