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