- Implemented live certificate reloading for haproxy using the runtime api. This does NOT remove deleted certificates from haproxy - Bump version to 0.2.0
110 lines
3.8 KiB
Rust
110 lines
3.8 KiB
Rust
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<T: AsRef<Path>>(&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::<Vec<_>>();
|
|
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<String, HaProxyErr> {
|
|
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)
|
|
}
|
|
}
|