Initial commit
This commit is contained in:
commit
f58886b89e
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
certs
|
||||||
1121
Cargo.lock
generated
Normal file
1121
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "le-easy-certs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
acme2 = "0.5"
|
||||||
|
env_logger = "0.9"
|
||||||
|
hyper = { version = "0.14", features = [ "server" ] }
|
||||||
|
log = "0.4"
|
||||||
|
serde = "1.0"
|
||||||
|
serde_derive = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread", "fs", "parking_lot" ] }
|
||||||
|
toml = "0.5"
|
||||||
263
src/certs.rs
Normal file
263
src/certs.rs
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
use crate::{config::ConfigCert, http::ChallengeManager};
|
||||||
|
use acme2::{
|
||||||
|
gen_rsa_private_key,
|
||||||
|
openssl::{asn1::Asn1Time, pkey::PKey, x509::X509},
|
||||||
|
Account, AccountBuilder, AuthorizationStatus, ChallengeStatus, Csr, Directory,
|
||||||
|
DirectoryBuilder, OrderBuilder, OrderStatus,
|
||||||
|
};
|
||||||
|
use log::{debug, info};
|
||||||
|
use std::{sync::Arc, time::Duration, fmt::Display};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ReqErr {
|
||||||
|
#[error("Invalid endpoint")]
|
||||||
|
InvaidEndpoint,
|
||||||
|
#[error("Account error")]
|
||||||
|
Account,
|
||||||
|
#[error("Order building error")]
|
||||||
|
OrderBuild,
|
||||||
|
#[error("Get Authorizations error")]
|
||||||
|
Authorizations,
|
||||||
|
#[error("Challenge error")]
|
||||||
|
Challenge,
|
||||||
|
#[error("Validation error")]
|
||||||
|
Validation,
|
||||||
|
#[error("Order error")]
|
||||||
|
Order,
|
||||||
|
#[error("Finalizing error")]
|
||||||
|
Finalizing,
|
||||||
|
#[error("No certificate chain error")]
|
||||||
|
NoChain,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience extension for X509 information in certs
|
||||||
|
pub trait CertExt {
|
||||||
|
/// Number of days until the certificate expires. This will be negative when the cert has
|
||||||
|
/// expired already
|
||||||
|
fn expires_in_days(&self) -> i32;
|
||||||
|
/// Get a list of all domain names for which this cert is valid
|
||||||
|
fn dns_names(&self) -> Vec<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ACME API endpoint that should be used. This can be the Letsencrypt production or tesing, as
|
||||||
|
/// well as any custom endpoint
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum AcmeApiEndpoint {
|
||||||
|
/// Letsencrypt Staging API (testing)
|
||||||
|
LetsEncryptStaging,
|
||||||
|
/// Letsencrypt Production API (valid certs)
|
||||||
|
LetsEncryptProduction,
|
||||||
|
/// Custom ACME API specified via URL
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CertRequester {
|
||||||
|
endpoint: AcmeApiEndpoint,
|
||||||
|
challenge_mgr: ChallengeManager,
|
||||||
|
conf: ConfigCert,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcmeApiEndpoint {
|
||||||
|
/// Get the actual URL for the endpoint as string
|
||||||
|
pub fn url(&self) -> String {
|
||||||
|
match self {
|
||||||
|
AcmeApiEndpoint::LetsEncryptStaging => {
|
||||||
|
"https://acme-staging-v02.api.letsencrypt.org/directory".to_string()
|
||||||
|
}
|
||||||
|
AcmeApiEndpoint::LetsEncryptProduction => {
|
||||||
|
"https://acme-v02.api.letsencrypt.org/directory".to_string()
|
||||||
|
}
|
||||||
|
AcmeApiEndpoint::Custom(url) => url.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AcmeApiEndpoint {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
AcmeApiEndpoint::LetsEncryptStaging => write!(f, "LetsEncryptStaging"),
|
||||||
|
AcmeApiEndpoint::LetsEncryptProduction => write!(f, "LetsEncryptProduction"),
|
||||||
|
AcmeApiEndpoint::Custom(url) => write!(f, "Custom({url})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CertRequester {
|
||||||
|
pub fn new(
|
||||||
|
endpoint: AcmeApiEndpoint,
|
||||||
|
conf: ConfigCert,
|
||||||
|
challenge_mgr: ChallengeManager,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
endpoint,
|
||||||
|
challenge_mgr,
|
||||||
|
conf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_or_create_account(
|
||||||
|
&self,
|
||||||
|
dir: Arc<Directory>,
|
||||||
|
) -> Result<Arc<Account>, Box<dyn std::error::Error>> {
|
||||||
|
let mut builder = AccountBuilder::new(dir);
|
||||||
|
|
||||||
|
let acc = match fs::read(&self.conf.account_file).await {
|
||||||
|
Ok(pem) => {
|
||||||
|
let key = PKey::private_key_from_pem(&pem)?;
|
||||||
|
builder.private_key(key).build().await?
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
println!("Creating new account!");
|
||||||
|
let acc = builder.terms_of_service_agreed(true).build().await?;
|
||||||
|
let key = acc.private_key().private_key_to_pem_pkcs8()?;
|
||||||
|
fs::write(&self.conf.account_file, &key).await?;
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_certs(&self) -> Result<(), ReqErr> {
|
||||||
|
let dir = DirectoryBuilder::new(self.endpoint.url())
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ReqErr::InvaidEndpoint)?;
|
||||||
|
|
||||||
|
let account = self
|
||||||
|
.load_or_create_account(dir)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ReqErr::Account)?;
|
||||||
|
|
||||||
|
let order = self
|
||||||
|
.conf
|
||||||
|
.domains
|
||||||
|
.iter()
|
||||||
|
.fold(&mut OrderBuilder::new(account), |acc, domain| {
|
||||||
|
acc.add_dns_identifier(domain.to_string())
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ReqErr::OrderBuild)?;
|
||||||
|
|
||||||
|
let authorizations = order
|
||||||
|
.authorizations()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ReqErr::Authorizations)?;
|
||||||
|
|
||||||
|
info!("Creating challenges");
|
||||||
|
for auth in authorizations {
|
||||||
|
let challenge = auth.get_challenge("http-01").ok_or(ReqErr::Challenge)?;
|
||||||
|
|
||||||
|
let token = challenge
|
||||||
|
.token
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(ReqErr::Challenge)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let key_auth = challenge
|
||||||
|
.key_authorization()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.ok_or(ReqErr::Challenge)?;
|
||||||
|
|
||||||
|
debug!("Interting challenge in manager: {token} => {key_auth}");
|
||||||
|
self.challenge_mgr.insert(token.clone(), key_auth).await;
|
||||||
|
|
||||||
|
challenge.validate().await.map_err(|_| ReqErr::Validation)?;
|
||||||
|
|
||||||
|
let challenge = challenge.wait_done(Duration::from_secs(5), 3).await.map_err(|_| ReqErr::Validation)?;
|
||||||
|
if !matches!(challenge.status, ChallengeStatus::Valid) {
|
||||||
|
return Err(ReqErr::Validation);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.challenge_mgr.remove(&token).await;
|
||||||
|
|
||||||
|
debug!("Wait for authorization");
|
||||||
|
let auth = auth.wait_done(Duration::from_secs(5), 3).await.map_err(|_| ReqErr::Validation)?;
|
||||||
|
if !matches!(auth.status, AuthorizationStatus::Valid) {
|
||||||
|
return Err(ReqErr::Validation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Waiting for order");
|
||||||
|
let order = order.wait_ready(Duration::from_secs(10), 5).await.map_err(|_| ReqErr::Order)?;
|
||||||
|
if !matches!(order.status, OrderStatus::Ready) {
|
||||||
|
return Err(ReqErr::Order);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pkey = gen_rsa_private_key(4096).unwrap();
|
||||||
|
|
||||||
|
info!("Finalizing certificate");
|
||||||
|
let order = order.finalize(Csr::Automatic(pkey.clone())).await.map_err(|_| ReqErr::Finalizing)?;
|
||||||
|
let order = order.wait_done(Duration::from_secs(5), 3).await.map_err(|_| ReqErr::Finalizing)?;
|
||||||
|
if !matches!(order.status, OrderStatus::Valid) {
|
||||||
|
return Err(ReqErr::Finalizing);
|
||||||
|
}
|
||||||
|
|
||||||
|
let certs = order.certificate().await.map_err(|_| ReqErr::Finalizing)?.ok_or(ReqErr::Finalizing)?;
|
||||||
|
|
||||||
|
if certs.len() <= 1 {
|
||||||
|
return Err(ReqErr::NoChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("The new certificate expires in {}", certs[0].expires_in_days());
|
||||||
|
|
||||||
|
let x = certs
|
||||||
|
.into_iter()
|
||||||
|
.map(|it| it.to_pem().unwrap())
|
||||||
|
.flatten()
|
||||||
|
.chain(pkey.private_key_to_pem_pkcs8().unwrap().into_iter())
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
|
|
||||||
|
tokio::fs::write(&self.conf.fullchain_file, x).await.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CertExt for X509 {
|
||||||
|
fn expires_in_days(&self) -> i32 {
|
||||||
|
let now = Asn1Time::days_from_now(0).unwrap();
|
||||||
|
let diff = now.diff(self.not_after()).unwrap();
|
||||||
|
diff.days
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dns_names(&self) -> Vec<String> {
|
||||||
|
let mut names = Vec::new();
|
||||||
|
|
||||||
|
if let Some(alt_names) = self.subject_alt_names() {
|
||||||
|
names.extend(
|
||||||
|
alt_names
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|it| it.dnsname().map(String::from)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_cert_from_fullchain(path: &str) -> Option<X509> {
|
||||||
|
const START: &str = "-----BEGIN CERTIFICATE-----";
|
||||||
|
const END: &str = "-----END CERTIFICATE-----";
|
||||||
|
|
||||||
|
let fullchain = tokio::fs::read_to_string(path).await.ok()?;
|
||||||
|
|
||||||
|
let mut fullchain = fullchain.as_str();
|
||||||
|
while let (Some(start), Some(end)) = (fullchain.find(START), fullchain.find(END)) {
|
||||||
|
let pem = &fullchain[start..end + END.len()];
|
||||||
|
fullchain = &fullchain[end + END.len()..];
|
||||||
|
|
||||||
|
let cert = X509::from_pem(pem.as_bytes()).unwrap();
|
||||||
|
if cert.dns_names().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
34
src/config.rs
Normal file
34
src/config.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use serde_derive::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub http: ConfigHttp,
|
||||||
|
pub certs: HashMap<String, ConfigCert>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Config for the http server used to serve the challenges
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ConfigHttp {
|
||||||
|
/// Binding IP address
|
||||||
|
pub ip: String,
|
||||||
|
/// Binding port
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for a single certificate
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ConfigCert {
|
||||||
|
/// Path to the account file to use for this certificate request
|
||||||
|
pub account_file: String,
|
||||||
|
/// Path to the fullchain certificate file that will be created or updated
|
||||||
|
pub fullchain_file: String,
|
||||||
|
/// List of domain names to include in the certificate
|
||||||
|
pub domains: Vec<String>,
|
||||||
|
/// Renew the certificate this many days before expiration
|
||||||
|
pub renew_days: i32,
|
||||||
|
/// Optional custom endpoint. If no enpoint is specified, the production letsencrypt endpoint
|
||||||
|
/// is used. "LetsEncryptStaging" and "LetsEncryptProduction" will use the corresponding
|
||||||
|
/// letsencrypt endpoints. Everything else has to be an actual URL specifying the endpoint.
|
||||||
|
pub endpoint: Option<String>,
|
||||||
|
}
|
||||||
114
src/http.rs
Normal file
114
src/http.rs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
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 log::info;
|
||||||
|
use tokio::sync::{RwLock, oneshot};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChallengeManager {
|
||||||
|
challenges: Arc<RwLock<HashMap<String, String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Http server to host the challenges
|
||||||
|
pub struct ChallengeServer {
|
||||||
|
mgr: ChallengeManager,
|
||||||
|
sock_addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChallengeManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let challenges = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
Self { challenges }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(&self, token: String, key: String) {
|
||||||
|
self.challenges.write().await.insert(token, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get(&self, token: &str) -> Option<String> {
|
||||||
|
self.challenges.read().await.get(token).map(String::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove(&self, token: &str) {
|
||||||
|
self.challenges.write().await.remove(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChallengeServer {
|
||||||
|
pub fn new(bind_ip: &str, bind_port: u16) -> Result<Self, AddrParseError> {
|
||||||
|
let mgr = ChallengeManager::new();
|
||||||
|
let sock_addr = format!("{}:{}", bind_ip, bind_port).parse()?;
|
||||||
|
|
||||||
|
Ok(Self { mgr, sock_addr })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone_challenge_mgr(&self) -> ChallengeManager {
|
||||||
|
self.mgr.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(&self) -> Result<(impl Future, oneshot::Sender<()>), hyper::Error> {
|
||||||
|
let mgr = self.clone_challenge_mgr();
|
||||||
|
let make_service = make_service_fn(move |_con| {
|
||||||
|
let mgr = mgr.clone();
|
||||||
|
async move {
|
||||||
|
let mgr = mgr.clone();
|
||||||
|
Ok::<_, Infallible>(service_fn(move |req: Request<Body>| {
|
||||||
|
let mgr = mgr.clone();
|
||||||
|
async move { Ok::<_, Infallible>(Self::serve_challenges(req, mgr).await) }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let srv = Server::bind(&self.sock_addr).serve(make_service);
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel::<()>();
|
||||||
|
|
||||||
|
let srv = srv.with_graceful_shutdown(async move {
|
||||||
|
let _ = rx.await;
|
||||||
|
});
|
||||||
|
Ok((srv, tx))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve_challenges(req: Request<Body>, mgr: ChallengeManager) -> Response<Body> {
|
||||||
|
info!("New http request: {}", req.uri());
|
||||||
|
|
||||||
|
if !matches!(req.method(), &Method::GET) {
|
||||||
|
info!("Request is not GET -> Reject");
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(405)
|
||||||
|
.body(String::new().into())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let challenge_prefix = "/.well-known/acme-challenge/";
|
||||||
|
|
||||||
|
let path = req.uri().path();
|
||||||
|
if !path.starts_with(challenge_prefix) {
|
||||||
|
info!("Request is not for /.well-known -> Reject");
|
||||||
|
|
||||||
|
return Response::builder()
|
||||||
|
.status(404)
|
||||||
|
.body(String::new().into())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let tok = &path[challenge_prefix.len()..];
|
||||||
|
info!("Requested token: {}", tok);
|
||||||
|
|
||||||
|
match mgr.get(tok).await {
|
||||||
|
Some(auth) => {
|
||||||
|
info!("Answering Request = {auth}");
|
||||||
|
Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(auth.to_string().into())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("No matching challenge");
|
||||||
|
Response::builder()
|
||||||
|
.status(404)
|
||||||
|
.body(String::new().into())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/main.rs
Normal file
81
src/main.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use log::{debug, error, info};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
certs::{load_cert_from_fullchain, AcmeApiEndpoint, CertExt, CertRequester},
|
||||||
|
config::Config,
|
||||||
|
http::ChallengeServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod certs;
|
||||||
|
mod config;
|
||||||
|
mod http;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "info"));
|
||||||
|
|
||||||
|
// Config path evaluation order:
|
||||||
|
// - first cli argument
|
||||||
|
// - LE_CONF environment variable
|
||||||
|
// ./le-conf.toml
|
||||||
|
let config_path = std::env::args()
|
||||||
|
.skip(1)
|
||||||
|
.next()
|
||||||
|
.unwrap_or_else(|| std::env::var("LE_CONF").unwrap_or("./le-conf.toml".to_string()));
|
||||||
|
|
||||||
|
info!("Loading config file '{}'", config_path);
|
||||||
|
let s_conf = tokio::fs::read_to_string(&config_path)
|
||||||
|
.await
|
||||||
|
.expect(&format!("Failed to load config file: {}", config_path));
|
||||||
|
let conf: Config = toml::from_str(&s_conf)?;
|
||||||
|
|
||||||
|
debug!("Config file: {:#?}", &conf);
|
||||||
|
|
||||||
|
// Create the http server for serving the challenges
|
||||||
|
let srv = ChallengeServer::new(&conf.http.ip, conf.http.port)?;
|
||||||
|
let mgr = srv.clone_challenge_mgr();
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
info!("Starting http server: {}:{}", &conf.http.ip, conf.http.port);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
srv.start().await.unwrap().0.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
// See what certs are requested in the config file
|
||||||
|
for (name, conf) in conf.certs {
|
||||||
|
// Check if the cert needs to be created / renewed
|
||||||
|
let should_renew = load_cert_from_fullchain(&conf.fullchain_file)
|
||||||
|
.await
|
||||||
|
.map(|cert| {
|
||||||
|
let expires_in_days = cert.expires_in_days();
|
||||||
|
let dns_names = cert.dns_names();
|
||||||
|
expires_in_days <= conf.renew_days || dns_names != conf.domains
|
||||||
|
})
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if should_renew {
|
||||||
|
let endpoint = conf
|
||||||
|
.endpoint
|
||||||
|
.as_ref()
|
||||||
|
.map(|ep| match ep.as_ref() {
|
||||||
|
"LetsEncryptProduction" => AcmeApiEndpoint::LetsEncryptProduction,
|
||||||
|
"LetsEncryptStaging" => AcmeApiEndpoint::LetsEncryptStaging,
|
||||||
|
url => AcmeApiEndpoint::Custom(url.to_string()),
|
||||||
|
})
|
||||||
|
.unwrap_or(AcmeApiEndpoint::LetsEncryptProduction);
|
||||||
|
|
||||||
|
info!("Certificate {name} needs to be renewed. Using endpoint: {endpoint}");
|
||||||
|
|
||||||
|
let requester = CertRequester::new(endpoint, conf, mgr.clone());
|
||||||
|
if let Err(e) = requester.request_certs().await {
|
||||||
|
error!("Certificate request for {name} failed: {e}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Certificate {name} does not need to be renewed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("All done. Shutting down");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user