Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -350,7 +350,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "le-easy-certs"
|
name = "le-easy-certs"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acme2",
|
"acme2",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "le-easy-certs"
|
name = "le-easy-certs"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
strip = "debuginfo"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acme2 = "0.5"
|
acme2 = "0.5"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
|
|||||||
36
Dockerfile
36
Dockerfile
@ -1,36 +0,0 @@
|
|||||||
FROM rust:1.59.0-slim-bullseye as build
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y libssl-dev pkg-config
|
|
||||||
|
|
||||||
COPY ./src ./src
|
|
||||||
COPY ./Cargo.toml ./
|
|
||||||
COPY ./Cargo.lock ./
|
|
||||||
|
|
||||||
RUN cargo build --release
|
|
||||||
|
|
||||||
#################
|
|
||||||
# RELEASE IMAGE #
|
|
||||||
#################
|
|
||||||
FROM debian:bullseye-slim
|
|
||||||
|
|
||||||
# Install cron and clean default cron jobs
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends cron && rm -rf /etc/cron.*/*
|
|
||||||
|
|
||||||
# Creating the file like this is very suboptimal to say the least
|
|
||||||
RUN echo "0 0,12 * * * root cd /app && ./le-easy-certs >/proc/1/fd/1 2>/proc/1/fd/2\n" > /etc/crontab
|
|
||||||
RUN echo "@reboot root cd /app && ./le-easy-certs >/proc/1/fd/1 2>/proc/1/fd/2\n\n" >> /etc/crontab
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build /app/target/release/le-easy-certs /app/
|
|
||||||
|
|
||||||
ENV LE_CONF=/le-conf/le-conf.toml
|
|
||||||
|
|
||||||
# Creating the file like this is very suboptimal to say the least
|
|
||||||
RUN echo "#!/bin/sh" > /app/run.sh
|
|
||||||
RUN echo "env >> /etc/environment" >> /app/run.sh
|
|
||||||
RUN echo "cron -f" >> /app/run.sh
|
|
||||||
RUN chmod +x /app/run.sh
|
|
||||||
|
|
||||||
CMD [ "/app/run.sh" ]
|
|
||||||
50
README.md
50
README.md
@ -1,50 +0,0 @@
|
|||||||
# Lé easy certs
|
|
||||||
> (Let's Encrypt easy certificates)
|
|
||||||
|
|
||||||
## Project state
|
|
||||||
This project is in a really early state and is in no way stable or ready for production.
|
|
||||||
|
|
||||||
## How to use
|
|
||||||
The program reads the configuration file and requests / renews the certificates according to the
|
|
||||||
configuration.
|
|
||||||
|
|
||||||
- If specified, the first CLI argument will be used as path for the config file
|
|
||||||
- If specified (and no CLI arg), the `LE_CONF` environment variable will be used as path for the
|
|
||||||
config file
|
|
||||||
- If nothing is specified, a config file `./le-conf.toml` will be used
|
|
||||||
|
|
||||||
## Example config
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[http]
|
|
||||||
ip = "0.0.0.0"
|
|
||||||
port = 80
|
|
||||||
|
|
||||||
[certs.example_cert1]
|
|
||||||
renew_days = 30
|
|
||||||
account_file = "./account.pem"
|
|
||||||
fullchain_file = "./example_com_fullchain.pem"
|
|
||||||
domains = [
|
|
||||||
"example.com",
|
|
||||||
"www.example.com",
|
|
||||||
"sub1.example.com",
|
|
||||||
"sub2.example.com",
|
|
||||||
]
|
|
||||||
|
|
||||||
[certs.example_cert2]
|
|
||||||
renew_days = 30
|
|
||||||
account_file = "./account.pem"
|
|
||||||
fullchain_file = "./example2_com_fullchain.pem"
|
|
||||||
domains = [
|
|
||||||
"example2.com",
|
|
||||||
"www.example2.com",
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
The docker image is not yet fully usable. The intention is to have a `le-easy-certs` container
|
|
||||||
running next to a a `haproxy` container and automatically renew / update the certificates when
|
|
||||||
needed. Also the certificates could be hot reloaded in `haproxy` at some point.
|
|
||||||
|
|
||||||
Right now the docker image could be used to run `le-easy-certs` manually / automated at fixed times
|
|
||||||
controlled by the host.
|
|
||||||
61
src/certs.rs
61
src/certs.rs
@ -6,7 +6,7 @@ use acme2::{
|
|||||||
DirectoryBuilder, OrderBuilder, OrderStatus,
|
DirectoryBuilder, OrderBuilder, OrderStatus,
|
||||||
};
|
};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use std::{collections::HashSet, fmt::Display, sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration, fmt::Display};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@ -29,8 +29,6 @@ pub enum ReqErr {
|
|||||||
Finalizing,
|
Finalizing,
|
||||||
#[error("No certificate chain error")]
|
#[error("No certificate chain error")]
|
||||||
NoChain,
|
NoChain,
|
||||||
#[error("Timeout: {0}")]
|
|
||||||
Timeout(Box<ReqErr>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience extension for X509 information in certs
|
/// Convenience extension for X509 information in certs
|
||||||
@ -39,7 +37,7 @@ pub trait CertExt {
|
|||||||
/// expired already
|
/// expired already
|
||||||
fn expires_in_days(&self) -> i32;
|
fn expires_in_days(&self) -> i32;
|
||||||
/// Get a list of all domain names for which this cert is valid
|
/// Get a list of all domain names for which this cert is valid
|
||||||
fn dns_names(&self) -> HashSet<String>;
|
fn dns_names(&self) -> Vec<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ACME API endpoint that should be used. This can be the Letsencrypt production or tesing, as
|
/// The ACME API endpoint that should be used. This can be the Letsencrypt production or tesing, as
|
||||||
@ -107,15 +105,11 @@ impl CertRequester {
|
|||||||
|
|
||||||
let acc = match fs::read(&self.conf.account_file).await {
|
let acc = match fs::read(&self.conf.account_file).await {
|
||||||
Ok(pem) => {
|
Ok(pem) => {
|
||||||
info!("Using account '{}'", &self.conf.account_file);
|
|
||||||
let key = PKey::private_key_from_pem(&pem)?;
|
let key = PKey::private_key_from_pem(&pem)?;
|
||||||
builder.private_key(key).build().await?
|
builder.private_key(key).build().await?
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
info!(
|
println!("Creating new account!");
|
||||||
"Account '{}' does not exists. Creating new",
|
|
||||||
&self.conf.account_file
|
|
||||||
);
|
|
||||||
let acc = builder.terms_of_service_agreed(true).build().await?;
|
let acc = builder.terms_of_service_agreed(true).build().await?;
|
||||||
let key = acc.private_key().private_key_to_pem_pkcs8()?;
|
let key = acc.private_key().private_key_to_pem_pkcs8()?;
|
||||||
fs::write(&self.conf.account_file, &key).await?;
|
fs::write(&self.conf.account_file, &key).await?;
|
||||||
@ -174,11 +168,7 @@ impl CertRequester {
|
|||||||
|
|
||||||
challenge.validate().await.map_err(|_| ReqErr::Validation)?;
|
challenge.validate().await.map_err(|_| ReqErr::Validation)?;
|
||||||
|
|
||||||
let challenge = challenge
|
let challenge = challenge.wait_done(Duration::from_secs(5), 3).await.map_err(|_| ReqErr::Validation)?;
|
||||||
.wait_done(Duration::from_secs(15), 3)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ReqErr::Timeout(ReqErr::Validation.into()))?;
|
|
||||||
|
|
||||||
if !matches!(challenge.status, ChallengeStatus::Valid) {
|
if !matches!(challenge.status, ChallengeStatus::Valid) {
|
||||||
return Err(ReqErr::Validation);
|
return Err(ReqErr::Validation);
|
||||||
}
|
}
|
||||||
@ -186,22 +176,14 @@ impl CertRequester {
|
|||||||
self.challenge_mgr.remove(&token).await;
|
self.challenge_mgr.remove(&token).await;
|
||||||
|
|
||||||
debug!("Wait for authorization");
|
debug!("Wait for authorization");
|
||||||
let auth = auth
|
let auth = auth.wait_done(Duration::from_secs(5), 3).await.map_err(|_| ReqErr::Validation)?;
|
||||||
.wait_done(Duration::from_secs(15), 3)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ReqErr::Timeout(ReqErr::Validation.into()))?;
|
|
||||||
|
|
||||||
if !matches!(auth.status, AuthorizationStatus::Valid) {
|
if !matches!(auth.status, AuthorizationStatus::Valid) {
|
||||||
return Err(ReqErr::Validation);
|
return Err(ReqErr::Validation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Waiting for order");
|
info!("Waiting for order");
|
||||||
let order = order
|
let order = order.wait_ready(Duration::from_secs(10), 5).await.map_err(|_| ReqErr::Order)?;
|
||||||
.wait_ready(Duration::from_secs(15), 5)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ReqErr::Timeout(ReqErr::Order.into()))?;
|
|
||||||
|
|
||||||
if !matches!(order.status, OrderStatus::Ready) {
|
if !matches!(order.status, OrderStatus::Ready) {
|
||||||
return Err(ReqErr::Order);
|
return Err(ReqErr::Order);
|
||||||
}
|
}
|
||||||
@ -209,34 +191,19 @@ impl CertRequester {
|
|||||||
let pkey = gen_rsa_private_key(4096).unwrap();
|
let pkey = gen_rsa_private_key(4096).unwrap();
|
||||||
|
|
||||||
info!("Finalizing certificate");
|
info!("Finalizing certificate");
|
||||||
let order = order
|
let order = order.finalize(Csr::Automatic(pkey.clone())).await.map_err(|_| ReqErr::Finalizing)?;
|
||||||
.finalize(Csr::Automatic(pkey.clone()))
|
let order = order.wait_done(Duration::from_secs(5), 3).await.map_err(|_| ReqErr::Finalizing)?;
|
||||||
.await
|
|
||||||
.map_err(|_| ReqErr::Finalizing)?;
|
|
||||||
|
|
||||||
let order = order
|
|
||||||
.wait_done(Duration::from_secs(15), 3)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ReqErr::Timeout(ReqErr::Finalizing.into()))?;
|
|
||||||
|
|
||||||
if !matches!(order.status, OrderStatus::Valid) {
|
if !matches!(order.status, OrderStatus::Valid) {
|
||||||
return Err(ReqErr::Finalizing);
|
return Err(ReqErr::Finalizing);
|
||||||
}
|
}
|
||||||
|
|
||||||
let certs = order
|
let certs = order.certificate().await.map_err(|_| ReqErr::Finalizing)?.ok_or(ReqErr::Finalizing)?;
|
||||||
.certificate()
|
|
||||||
.await
|
|
||||||
.map_err(|_| ReqErr::Finalizing)?
|
|
||||||
.ok_or(ReqErr::Finalizing)?;
|
|
||||||
|
|
||||||
if certs.len() <= 1 {
|
if certs.len() <= 1 {
|
||||||
return Err(ReqErr::NoChain);
|
return Err(ReqErr::NoChain);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!("The new certificate expires in {}", certs[0].expires_in_days());
|
||||||
"The new certificate expires in {} days",
|
|
||||||
certs[0].expires_in_days()
|
|
||||||
);
|
|
||||||
|
|
||||||
let x = certs
|
let x = certs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -245,9 +212,7 @@ impl CertRequester {
|
|||||||
.chain(pkey.private_key_to_pem_pkcs8().unwrap().into_iter())
|
.chain(pkey.private_key_to_pem_pkcs8().unwrap().into_iter())
|
||||||
.collect::<Vec<u8>>();
|
.collect::<Vec<u8>>();
|
||||||
|
|
||||||
tokio::fs::write(&self.conf.fullchain_file, x)
|
tokio::fs::write(&self.conf.fullchain_file, x).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -260,8 +225,8 @@ impl CertExt for X509 {
|
|||||||
diff.days
|
diff.days
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dns_names(&self) -> HashSet<String> {
|
fn dns_names(&self) -> Vec<String> {
|
||||||
let mut names = HashSet::new();
|
let mut names = Vec::new();
|
||||||
|
|
||||||
if let Some(alt_names) = self.subject_alt_names() {
|
if let Some(alt_names) = self.subject_alt_names() {
|
||||||
names.extend(
|
names.extend(
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
use serde_derive::Deserialize;
|
use serde_derive::Deserialize;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub http: ConfigHttp,
|
pub http: ConfigHttp,
|
||||||
pub certs: HashMap<String, ConfigCert>,
|
pub certs: HashMap<String, ConfigCert>,
|
||||||
pub haproxy: Option<ConfigHaProxyRuntime>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Config for the http server used to serve the challenges
|
/// Config for the http server used to serve the challenges
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ConfigHttp {
|
pub struct ConfigHttp {
|
||||||
/// Binding IP address
|
/// Binding IP address
|
||||||
pub ip: String,
|
pub ip: String,
|
||||||
@ -17,26 +16,15 @@ pub struct ConfigHttp {
|
|||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Config for a haproxy runtime api endpoint
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct ConfigHaProxyRuntime {
|
|
||||||
/// Ip address (or hostname) for the haproxy runtime api
|
|
||||||
pub ip: String,
|
|
||||||
/// Port for the haproxy runtime api
|
|
||||||
pub port: u16,
|
|
||||||
/// The directory path on the haproxy host that contains the certificates
|
|
||||||
pub cert_dir: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for a single certificate
|
/// Configuration for a single certificate
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ConfigCert {
|
pub struct ConfigCert {
|
||||||
/// Path to the account file to use for this certificate request
|
/// Path to the account file to use for this certificate request
|
||||||
pub account_file: String,
|
pub account_file: String,
|
||||||
/// Path to the fullchain certificate file that will be created or updated
|
/// Path to the fullchain certificate file that will be created or updated
|
||||||
pub fullchain_file: String,
|
pub fullchain_file: String,
|
||||||
/// List of domain names to include in the certificate
|
/// List of domain names to include in the certificate
|
||||||
pub domains: HashSet<String>,
|
pub domains: Vec<String>,
|
||||||
/// Renew the certificate this many days before expiration
|
/// Renew the certificate this many days before expiration
|
||||||
pub renew_days: i32,
|
pub renew_days: i32,
|
||||||
/// Optional custom endpoint. If no enpoint is specified, the production letsencrypt endpoint
|
/// Optional custom endpoint. If no enpoint is specified, the production letsencrypt endpoint
|
||||||
|
|||||||
150
src/haproxy.rs
150
src/haproxy.rs
@ -1,150 +0,0 @@
|
|||||||
use log::debug;
|
|
||||||
use std::{path::Path, time::Duration};
|
|
||||||
use tokio::{
|
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
|
||||||
time::timeout,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum HaProxyErr {
|
|
||||||
#[error("Connection error")]
|
|
||||||
Connection,
|
|
||||||
#[error("Update failed")]
|
|
||||||
UpdateFailed,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
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>>(&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_cert_path = format!("{}/{cert_name}", &self.cert_dir);
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Trying to update haproxy cert in memory: {}/{cert_name} <- {}",
|
|
||||||
&self.cert_dir,
|
|
||||||
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.abort_cert(transaction).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.new_cert(&haproxy_cert_path).await?;
|
|
||||||
self.set_cert(&haproxy_cert_path, &cert_content).await?;
|
|
||||||
self.commit_ssl_cert(&haproxy_cert_path).await?;
|
|
||||||
self.add_ssl_crt_list(&self.cert_dir, &haproxy_cert_path)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See: https://www.haproxy.com/documentation/hapee/latest/api/runtime-api/abort-ssl-cert/
|
|
||||||
pub async fn abort_cert(&self, cert_path: &str) -> Result<(), HaProxyErr> {
|
|
||||||
self.send(&format!("abort ssl cert {cert_path}")).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See: https://www.haproxy.com/documentation/hapee/latest/api/runtime-api/new-ssl-cert/
|
|
||||||
pub async fn new_cert(&self, cert_path: &str) -> Result<(), HaProxyErr> {
|
|
||||||
let resp = self.send(&format!("new ssl cert {cert_path}")).await?;
|
|
||||||
if !resp.contains("already exists") && !resp.contains("New empty certificate store ") {
|
|
||||||
return Err(HaProxyErr::UpdateFailed);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See: https://www.haproxy.com/documentation/hapee/latest/api/runtime-api/set-ssl-cert/
|
|
||||||
pub async fn set_cert(&self, cert_path: &str, cert_content: &str) -> Result<(), HaProxyErr> {
|
|
||||||
let resp = self
|
|
||||||
.send(&format!("set ssl cert {cert_path} <<\n{cert_content}"))
|
|
||||||
.await?;
|
|
||||||
if !resp.contains("Transaction created") {
|
|
||||||
return Err(HaProxyErr::UpdateFailed);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See: https://www.haproxy.com/documentation/hapee/latest/api/runtime-api/commit-ssl-cert/
|
|
||||||
pub async fn commit_ssl_cert(&self, cert_path: &str) -> Result<(), HaProxyErr> {
|
|
||||||
let resp = self.send(&format!("commit ssl cert {cert_path}")).await?;
|
|
||||||
if !resp.contains("Success") {
|
|
||||||
self.send(&format!("abort ssl cert {cert_path}")).await?;
|
|
||||||
return Err(HaProxyErr::UpdateFailed);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See: https://www.haproxy.com/documentation/hapee/latest/api/runtime-api/add-ssl-crt-list/
|
|
||||||
pub async fn add_ssl_crt_list(
|
|
||||||
&self,
|
|
||||||
list_path: &str,
|
|
||||||
cert_path: &str,
|
|
||||||
) -> Result<(), HaProxyErr> {
|
|
||||||
let resp = self
|
|
||||||
.send(&format!("add ssl crt-list {list_path} <<\n{cert_path}\n"))
|
|
||||||
.await?;
|
|
||||||
if !resp.contains("already exists") && !resp.contains("Inserting certificate") {
|
|
||||||
return Err(HaProxyErr::UpdateFailed);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a string command to the haproxy runtime api and return the response string
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
src/http.rs
14
src/http.rs
@ -1,6 +1,6 @@
|
|||||||
use std::{sync::Arc, collections::HashMap, net::{SocketAddr, AddrParseError}, convert::Infallible, future::Future};
|
use std::{sync::Arc, collections::HashMap, net::{SocketAddr, AddrParseError}, convert::Infallible, future::Future};
|
||||||
use hyper::{Server, service::{make_service_fn, service_fn}, Request, Body, Response, Method};
|
use hyper::{Server, service::{make_service_fn, service_fn}, Request, Body, Response, Method};
|
||||||
use log::debug;
|
use log::info;
|
||||||
use tokio::sync::{RwLock, oneshot};
|
use tokio::sync::{RwLock, oneshot};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -68,10 +68,10 @@ impl ChallengeServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_challenges(req: Request<Body>, mgr: ChallengeManager) -> Response<Body> {
|
async fn serve_challenges(req: Request<Body>, mgr: ChallengeManager) -> Response<Body> {
|
||||||
debug!("New http request: {}", req.uri());
|
info!("New http request: {}", req.uri());
|
||||||
|
|
||||||
if !matches!(req.method(), &Method::GET) {
|
if !matches!(req.method(), &Method::GET) {
|
||||||
debug!("Request is not GET -> Reject");
|
info!("Request is not GET -> Reject");
|
||||||
|
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
.status(405)
|
.status(405)
|
||||||
@ -83,7 +83,7 @@ impl ChallengeServer {
|
|||||||
|
|
||||||
let path = req.uri().path();
|
let path = req.uri().path();
|
||||||
if !path.starts_with(challenge_prefix) {
|
if !path.starts_with(challenge_prefix) {
|
||||||
debug!("Request is not for /.well-known -> Reject");
|
info!("Request is not for /.well-known -> Reject");
|
||||||
|
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
.status(404)
|
.status(404)
|
||||||
@ -92,18 +92,18 @@ impl ChallengeServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let tok = &path[challenge_prefix.len()..];
|
let tok = &path[challenge_prefix.len()..];
|
||||||
debug!("Requested token: {}", tok);
|
info!("Requested token: {}", tok);
|
||||||
|
|
||||||
match mgr.get(tok).await {
|
match mgr.get(tok).await {
|
||||||
Some(auth) => {
|
Some(auth) => {
|
||||||
debug!("Answering Request = {auth}");
|
info!("Answering Request = {auth}");
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
.body(auth.to_string().into())
|
.body(auth.to_string().into())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
debug!("No matching challenge");
|
info!("No matching challenge");
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(404)
|
.status(404)
|
||||||
.body(String::new().into())
|
.body(String::new().into())
|
||||||
|
|||||||
32
src/main.rs
32
src/main.rs
@ -3,13 +3,12 @@ use log::{debug, error, info};
|
|||||||
use crate::{
|
use crate::{
|
||||||
certs::{load_cert_from_fullchain, AcmeApiEndpoint, CertExt, CertRequester},
|
certs::{load_cert_from_fullchain, AcmeApiEndpoint, CertExt, CertRequester},
|
||||||
config::Config,
|
config::Config,
|
||||||
http::{ChallengeServer, ChallengeManager}, haproxy::HaProxyApi,
|
http::ChallengeServer,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod certs;
|
mod certs;
|
||||||
mod config;
|
mod config;
|
||||||
mod http;
|
mod http;
|
||||||
mod haproxy;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@ -32,8 +31,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
debug!("Config file: {:#?}", &conf);
|
debug!("Config file: {:#?}", &conf);
|
||||||
|
|
||||||
let haproxyapi = conf.clone().haproxy.map(|ha| HaProxyApi::new(&ha.ip, ha.port, &ha.cert_dir));
|
|
||||||
|
|
||||||
// Create the http server for serving the challenges
|
// Create the http server for serving the challenges
|
||||||
let srv = ChallengeServer::new(&conf.http.ip, conf.http.port)?;
|
let srv = ChallengeServer::new(&conf.http.ip, conf.http.port)?;
|
||||||
let mgr = srv.clone_challenge_mgr();
|
let mgr = srv.clone_challenge_mgr();
|
||||||
@ -44,16 +41,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
srv.start().await.unwrap().0.await;
|
srv.start().await.unwrap().0.await;
|
||||||
});
|
});
|
||||||
|
|
||||||
check_update_certs(&conf, &mgr, &haproxyapi).await;
|
|
||||||
|
|
||||||
info!("All done. Shutting down");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_update_certs(conf: &Config, mgr: &ChallengeManager, haproxyapi: &Option<HaProxyApi>) {
|
|
||||||
// See what certs are requested in the config file
|
// See what certs are requested in the config file
|
||||||
for (name, conf) in &conf.certs {
|
for (name, conf) in conf.certs {
|
||||||
// Check if the cert needs to be created / renewed
|
// Check if the cert needs to be created / renewed
|
||||||
let should_renew = load_cert_from_fullchain(&conf.fullchain_file)
|
let should_renew = load_cert_from_fullchain(&conf.fullchain_file)
|
||||||
.await
|
.await
|
||||||
@ -77,19 +66,16 @@ pub async fn check_update_certs(conf: &Config, mgr: &ChallengeManager, haproxyap
|
|||||||
|
|
||||||
info!("Certificate {name} needs to be renewed. Using endpoint: {endpoint}");
|
info!("Certificate {name} needs to be renewed. Using endpoint: {endpoint}");
|
||||||
|
|
||||||
let requester = CertRequester::new(endpoint, conf.clone(), mgr.clone());
|
let requester = CertRequester::new(endpoint, conf, mgr.clone());
|
||||||
match (requester.request_certs().await, haproxyapi) {
|
if let Err(e) = requester.request_certs().await {
|
||||||
(Ok(_), Some(api)) => {
|
error!("Certificate request for {name} failed: {e}");
|
||||||
match api.update_cert(&conf.fullchain_file).await {
|
|
||||||
Ok(()) => info!("Certificate update in haproxy completed"),
|
|
||||||
Err(e) => error!("Certificate update in haproxy failed: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Err(e), _) => error!("Certificate request for {name} failed: {e}"),
|
|
||||||
_ => ()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
info!("Certificate {name} does not need to be renewed");
|
info!("Certificate {name} does not need to be renewed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!("All done. Shutting down");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user