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