use std::net::{IpAddr, TcpStream};
use std::sync::Arc;
use std::time::Duration;
use rustls::pki_types::ServerName;
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use serde_json::Value;
pub mod entries;
pub mod frame;
pub mod hello;
pub mod leaf;
pub mod material;
pub mod tls;
use material::Material;
const READ_TIMEOUT: Duration = Duration::from_mins(2);
#[derive(Debug)]
pub struct Channel {
config: Arc<ClientConfig>,
address: String,
name: ServerName<'static>,
client: String,
}
impl Channel {
pub fn open(m: &Material) -> Result<Self, String> {
let client = leaf::foot(&m.chain)?;
Ok(Self {
config: tls::client_config(m)?,
address: m.address.clone(),
name: server_name(&m.address)?,
client,
})
}
pub fn client(&self) -> String {
self.client.clone()
}
pub fn address(&self) -> String {
self.address.clone()
}
pub 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| self.failed("receive", &e))? {
Some(chunk) => stream.push(chunk),
None => return Ok(stream),
}
}
}
fn failed(&self, leg: &str, e: &std::io::Error) -> String {
format!(
"{leg} {}: the channel to the engine failed. What the {leg} \
reported: {e}",
self.address
)
}
fn dial(&self, request: &Value) -> Result<StreamOwned<ClientConnection, TcpStream>, String> {
let tcp = TcpStream::connect(&self.address)
.and_then(|tcp| tcp.set_read_timeout(Some(READ_TIMEOUT)).map(|()| tcp))
.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);
hello::state(&mut tls).map_err(|e| self.failed("send", &e))?;
frame::write_value(&mut tls, request).map_err(|e| self.failed("send", &e))?;
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}"))
}
#[cfg(test)]
mod tests;