1use axum::extract::State;
4use axum::http::header::HeaderMap;
5use instant_acme::{self as acme, Account};
6use pem;
7use rustls::crypto::CryptoProvider;
8use rustls::sign::CertifiedKey;
9use rustls_pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
10use serde_json;
11use std::sync::Arc;
12use x509_parser::parse_x509_certificate;
13
14use crate::prelude::*;
15use crate::scheduler::{Task, TaskId};
16use cloudillo_types::auth_adapter;
17
18use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug)]
22struct X509CertData {
23 private_key_pem: Box<str>,
24 certificate_pem: Box<str>,
25 expires_at: Timestamp,
26}
27
28pub async fn init(
29 state: App,
30 acme_email: &str,
31 id_tag: &str,
32 app_domain: Option<&str>,
33) -> ClResult<()> {
34 info!("ACME init {}", acme_email);
35
36 let (account, credentials) = Account::builder()?
37 .create(
38 &acme::NewAccount {
39 contact: &[],
40 terms_of_service_agreed: true,
41 only_return_existing: false,
42 },
43 acme::LetsEncrypt::Production.url().to_owned(),
45 None,
46 )
47 .await?;
48 info!("ACME credentials {}", serde_json::to_string_pretty(&credentials)?);
49
50 let tn_id = state.auth_adapter.read_tn_id(id_tag).await?;
52 renew_tenant(state, &account, id_tag, tn_id.0, app_domain).await?;
53
54 Ok(())
55}
56
57pub async fn renew_tenant<'a>(
58 state: App,
59 account: &'a acme::Account,
60 id_tag: &'a str,
61 tn_id: u32,
62 app_domain: Option<&'a str>,
63) -> ClResult<()> {
64 let mut domains: Vec<String> = vec!["cl-o.".to_string() + id_tag];
65 if let Some(app_domain) = app_domain {
66 domains.push(app_domain.to_string());
67 } else {
68 info!("cloudillo app domain: {}", &id_tag);
69 domains.push(id_tag.into());
70 }
71
72 let cert = renew_domains(&state, account, domains).await?;
73 info!("ACME cert {}", &cert.expires_at);
74 state
75 .auth_adapter
76 .create_cert(&auth_adapter::CertData {
77 tn_id: TnId(tn_id),
78 id_tag: id_tag.into(),
79 domain: app_domain.unwrap_or(id_tag).into(),
80 key: cert.private_key_pem,
81 cert: cert.certificate_pem,
82 expires_at: cert.expires_at,
83 })
84 .await?;
85
86 Ok(())
87}
88
89async fn renew_domains<'a>(
91 state: &'a App,
92 account: &'a acme::Account,
93 domains: Vec<String>,
94) -> ClResult<X509CertData> {
95 info!("ACME {:?}", &domains);
96 let identifiers = domains
97 .iter()
98 .map(|domain| acme::Identifier::Dns(domain.to_string()))
99 .collect::<Vec<_>>();
100
101 let mut order = account.new_order(&acme::NewOrder::new(identifiers.as_slice())).await?;
102
103 info!("ACME order {:#?}", order.state());
104
105 if order.state().status == acme::OrderStatus::Pending {
106 let mut authorizations = order.authorizations();
107 while let Some(result) = authorizations.next().await {
108 let mut authz = result?;
109 match authz.status {
110 acme::AuthorizationStatus::Pending => {}
111 acme::AuthorizationStatus::Valid => continue,
112 status => {
113 warn!("Unexpected ACME authorization status: {:?}", status);
115 continue;
116 }
117 }
118
119 let mut challenge = authz
120 .challenge(acme::ChallengeType::Http01)
121 .ok_or(acme::Error::Str("no challenge"))?;
122 let identifier = challenge.identifier().to_string().into_boxed_str();
123 let token: Box<str> = challenge.key_authorization().as_str().into();
124 info!("ACME challenge {} {}", identifier, token);
125 state
126 .acme_challenge_map
127 .write()
128 .map_err(|_| {
129 Error::ServiceUnavailable("failed to access ACME challenge map".into())
130 })?
131 .insert(identifier.clone(), token);
132
133 challenge.set_ready().await?;
134 }
135
136 info!("Start polling...");
137 let retry_policy = acme::RetryPolicy::new()
141 .initial_delay(std::time::Duration::from_secs(1))
142 .backoff(1.5)
143 .timeout(std::time::Duration::from_secs(90));
144
145 let status = order.poll_ready(&retry_policy).await?;
146
147 if status != acme::OrderStatus::Ready {
148 let mut authorizations = order.authorizations();
150 while let Some(result) = authorizations.next().await {
151 if let Ok(authz) = result {
152 for challenge in &authz.challenges {
153 if challenge.r#type == acme::ChallengeType::Http01 {
154 if let Some(ref err) = challenge.error {
155 warn!(
156 "ACME validation failed for {}: {}",
157 authz.identifier(),
158 err.detail.as_deref().unwrap_or("unknown error")
159 );
160 }
161 }
162 }
163 }
164 }
165 Err(acme::Error::Str("order not ready"))?;
166 }
167
168 info!("Finalizing...");
169 let private_key_pem = order.finalize().await?;
170 let cert_chain_pem = order.poll_certificate(&retry_policy).await?;
172 info!("Got cert.");
173
174 for domain in domains.iter() {
176 state
177 .acme_challenge_map
178 .write()
179 .map_err(|_| {
180 Error::ServiceUnavailable("failed to access ACME challenge map".into())
181 })?
182 .remove(domain.as_str());
183 }
184
185 let pem = &pem::parse(&cert_chain_pem)?;
186 let cert_der = pem.contents();
187 let (_, parsed_cert) = parse_x509_certificate(cert_der)?;
188 let not_after = parsed_cert.validity().not_after;
189
190 let certified_key = Arc::new(CertifiedKey::from_der(
191 CertificateDer::pem_slice_iter(cert_chain_pem.as_bytes())
192 .filter_map(Result::ok)
193 .collect(),
194 PrivateKeyDer::from_pem_slice(private_key_pem.as_bytes())?,
195 CryptoProvider::get_default().ok_or(acme::Error::Str("no crypto provider"))?,
196 )?);
197 for domain in domains.iter() {
198 state
199 .certs
200 .write()
201 .map_err(|_| Error::ServiceUnavailable("failed to access cert cache".into()))?
202 .insert(domain.clone().into_boxed_str(), certified_key.clone());
203 }
204
205 let cert_data = X509CertData {
206 private_key_pem: private_key_pem.to_string().into_boxed_str(),
207 certificate_pem: cert_chain_pem.to_string().into_boxed_str(),
208 expires_at: Timestamp(not_after.timestamp()),
209 };
210
211 Ok(cert_data)
212 } else {
213 Err(Error::ConfigError("ACME initialization failed".into()))
214 }
215}
216
217pub async fn get_acme_challenge(
218 State(state): State<App>,
219 headers: HeaderMap,
220) -> ClResult<Box<str>> {
221 let domain = headers
222 .get("host")
223 .ok_or(Error::ValidationError("missing host header".into()))?
224 .to_str()?;
225 info!("ACME challenge for domain {:?}", domain);
226
227 if let Some(token) = state
228 .acme_challenge_map
229 .read()
230 .map_err(|_| Error::ServiceUnavailable("failed to access ACME challenge map".into()))?
231 .get(domain)
232 {
233 println!(" -> {:?}", &token);
234 Ok(token.clone())
235 } else {
236 println!(" -> not found");
237 Err(Error::PermissionDenied)
238 }
239}
240
241pub async fn renew_proxy_site_cert(app: &App, site_id: i64, domain: &str) -> ClResult<()> {
247 let (account, _credentials) = Account::builder()?
248 .create(
249 &acme::NewAccount {
250 contact: &[],
251 terms_of_service_agreed: true,
252 only_return_existing: false,
253 },
254 acme::LetsEncrypt::Production.url().to_owned(),
255 None,
256 )
257 .await?;
258
259 let domains = vec![domain.to_string()];
260 let cert = renew_domains(app, &account, domains).await?;
261
262 app.auth_adapter
263 .update_proxy_site_cert(
264 site_id,
265 &cert.certificate_pem,
266 &cert.private_key_pem,
267 cert.expires_at,
268 )
269 .await?;
270
271 info!(domain = %domain, "Proxy site certificate renewed successfully");
275 Ok(())
276}
277
278#[derive(Clone, Debug, Serialize, Deserialize)]
286pub struct CertRenewalTask {
287 pub renewal_days: u32,
289 pub acme_email: String,
291}
292
293impl CertRenewalTask {
294 pub fn new(acme_email: String, renewal_days: u32) -> Self {
296 Self { acme_email, renewal_days }
297 }
298}
299
300#[async_trait]
301impl Task<App> for CertRenewalTask {
302 fn kind() -> &'static str {
303 "acme.cert_renewal"
304 }
305
306 fn kind_of(&self) -> &'static str {
307 Self::kind()
308 }
309
310 fn build(_id: TaskId, context: &str) -> ClResult<Arc<dyn Task<App>>> {
311 let task: CertRenewalTask = serde_json::from_str(context).map_err(|e| {
312 Error::ValidationError(format!("Failed to deserialize cert renewal task: {}", e))
313 })?;
314 Ok(Arc::new(task))
315 }
316
317 fn serialize(&self) -> String {
318 serde_json::to_string(self)
319 .unwrap_or_else(|_| format!("acme.cert_renewal:{}", self.renewal_days))
320 }
321
322 async fn run(&self, app: &App) -> ClResult<()> {
323 info!("Running certificate renewal check (renewal threshold: {} days)", self.renewal_days);
324
325 let tenants = app.auth_adapter.list_tenants_needing_cert_renewal(self.renewal_days).await?;
327
328 if tenants.is_empty() {
329 info!("All tenant certificates are valid");
330 }
331
332 if !tenants.is_empty() {
333 info!("Found {} tenant(s) needing certificate renewal", tenants.len());
334
335 for (tn_id, id_tag) in tenants {
337 info!("Renewing certificate for tenant: {} (tn_id={})", id_tag, tn_id.0);
338
339 let app_domain = if tn_id.0 == 1 {
341 None
344 } else {
345 None
346 };
347
348 match init(app.clone(), &self.acme_email, &id_tag, app_domain).await {
350 Ok(_) => {
351 info!(tenant = %id_tag, "Certificate renewed successfully");
352 }
353 Err(e) => {
354 error!(tenant = %id_tag, error = %e, "Failed to renew certificate");
355 }
357 }
358 }
359 }
360
361 let proxy_sites = app
363 .auth_adapter
364 .list_proxy_sites_needing_cert_renewal(self.renewal_days)
365 .await?;
366
367 if !proxy_sites.is_empty() {
368 info!("Found {} proxy site(s) needing certificate renewal", proxy_sites.len());
369
370 for site in proxy_sites {
371 info!(
372 "Renewing certificate for proxy site: {} (site_id={})",
373 site.domain, site.site_id
374 );
375
376 if let Err(e) = renew_proxy_site_cert(app, site.site_id, &site.domain).await {
377 error!(
378 domain = %site.domain,
379 error = %e,
380 "Failed to renew proxy site certificate"
381 );
382 }
383 }
384 }
385
386 info!("Certificate renewal check completed");
387 Ok(())
388 }
389}
390
391pub fn register_tasks(app: &App) -> ClResult<()> {
395 app.scheduler.register::<CertRenewalTask>()?;
396 Ok(())
397}
398
399