use std::{path::Path, time::Duration}; use log::debug; use tokio::{io::{AsyncReadExt, AsyncWriteExt}, time::timeout}; #[derive(thiserror::Error, Debug)] pub enum HaProxyErr { #[error("Connection error")] Connection, #[error("Update failed")] UpdateFailed, } pub struct HaProxyApi { ip: String, port: u16, cert_dir: String, } impl HaProxyApi { pub fn new(ip: &str, port: u16, cert_dir: &str) -> Self { let ip = ip.to_string(); let cert_dir = cert_dir.to_string(); Self { ip, port, cert_dir } } pub async fn update_cert>(&mut self, cert_path: T) -> Result<(), HaProxyErr> { let cert_path = cert_path.as_ref(); let cert_name = cert_path.file_name().unwrap().to_string_lossy().to_string(); let cert_content = tokio::fs::read_to_string(cert_path).await.unwrap(); let haproxy_ssl_dir = &self.cert_dir; debug!("Trying to update haproxy cert in memory: {haproxy_ssl_dir}/{cert_name} <- {}", cert_path.display()); // Check for active transactions and abort them let resp = self.send(&format!("show ssl cert")).await?; let transactions = resp.lines().filter(|line| line.starts_with("*")).map(|line| &line[1..]).collect::>(); for transaction in transactions { self.send(&format!("abort ssl cert {transaction}")).await?; } // Try to create a new certificate in memory. This will do nothing when existing let resp = self.send(&format!("new ssl cert {haproxy_ssl_dir}/{cert_name}")).await?; if !resp.contains("already exists") && !resp.contains("New empty certificate store ") { return Err(HaProxyErr::UpdateFailed); } // Set the cert data in memory to the new cert. This beginns a transaction let resp = self.send(&format!("set ssl cert {haproxy_ssl_dir}/{cert_name} <<\n{cert_content}")).await?; if !resp.contains("Transaction created") { return Err(HaProxyErr::UpdateFailed); } // Commit the cert data transaction let resp = self.send(&format!("commit ssl cert {haproxy_ssl_dir}/{cert_name}")).await?; if !resp.contains("Success") { self.send(&format!("abort ssl cert {haproxy_ssl_dir}/{cert_name}")).await?; return Err(HaProxyErr::UpdateFailed); } // Try to add the certificate to the active list. This will do nothing if it is contained already let resp = self.send(&format!("add ssl crt-list {haproxy_ssl_dir} <<\n{haproxy_ssl_dir}/{cert_name}\n")).await?; if !resp.contains("already exists") && !resp.contains("Inserting certificate") { return Err(HaProxyErr::UpdateFailed); } Ok(()) } pub async fn send(&self, cmd: &str) -> Result { debug!("[Haproxy] send> '{cmd}'"); let mut con = tokio::net::TcpStream::connect((self.ip.as_str(), self.port)) .await .map_err(|_| HaProxyErr::Connection)?; con.write_all(cmd.as_bytes()) .await .map_err(|_| HaProxyErr::Connection)?; con.write_all(&[b'\n']) .await .map_err(|_| HaProxyErr::Connection)?; con.flush().await.unwrap(); let mut response = Vec::new(); let mut buf = [0_u8; 1024]; loop { match timeout(Duration::from_millis(5_000), con.read(&mut buf)).await { // No bytes read, or timeout Ok(Ok(0)) | Err(_) => break, Ok(Ok(read)) => { response.extend_from_slice(&buf[..read]); } Ok(Err(_)) => return Err(HaProxyErr::Connection), } } let response = String::from_utf8_lossy(&response).to_string(); debug!("[Haproxy] receive> '{response}'"); Ok(response) } }