BREAKING CHANGE(smart-proxy): move certificate persistence to an in-memory store and introduce consumer-managed certStore API; add default self-signed fallback cert and change ACME account handling
This commit is contained in:
@@ -29,9 +29,6 @@ pub struct AcmeOptions {
|
||||
/// Enable automatic renewal (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_renew: Option<bool>,
|
||||
/// Directory to store certificates (default: './certs')
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub certificate_store: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub skip_configured_certs: Option<bool>,
|
||||
/// How often to check for renewals (default: 24)
|
||||
@@ -361,7 +358,6 @@ mod tests {
|
||||
use_production: None,
|
||||
renew_threshold_days: None,
|
||||
auto_renew: None,
|
||||
certificate_store: None,
|
||||
skip_configured_certs: None,
|
||||
renew_check_interval_hours: None,
|
||||
}),
|
||||
|
||||
@@ -88,8 +88,8 @@ pub struct TcpListenerManager {
|
||||
route_manager: Arc<ArcSwap<RouteManager>>,
|
||||
/// Shared metrics collector
|
||||
metrics: Arc<MetricsCollector>,
|
||||
/// TLS acceptors indexed by domain
|
||||
tls_configs: Arc<HashMap<String, TlsCertConfig>>,
|
||||
/// TLS acceptors indexed by domain (ArcSwap for hot-reload visibility in accept loops)
|
||||
tls_configs: Arc<ArcSwap<HashMap<String, TlsCertConfig>>>,
|
||||
/// HTTP proxy service for HTTP-level forwarding
|
||||
http_proxy: Arc<HttpProxyService>,
|
||||
/// Connection configuration
|
||||
@@ -118,7 +118,7 @@ impl TcpListenerManager {
|
||||
listeners: HashMap::new(),
|
||||
route_manager: Arc::new(ArcSwap::from(route_manager)),
|
||||
metrics,
|
||||
tls_configs: Arc::new(HashMap::new()),
|
||||
tls_configs: Arc::new(ArcSwap::from(Arc::new(HashMap::new()))),
|
||||
http_proxy,
|
||||
conn_config: Arc::new(conn_config),
|
||||
conn_tracker,
|
||||
@@ -142,7 +142,7 @@ impl TcpListenerManager {
|
||||
listeners: HashMap::new(),
|
||||
route_manager: Arc::new(ArcSwap::from(route_manager)),
|
||||
metrics,
|
||||
tls_configs: Arc::new(HashMap::new()),
|
||||
tls_configs: Arc::new(ArcSwap::from(Arc::new(HashMap::new()))),
|
||||
http_proxy,
|
||||
conn_config: Arc::new(conn_config),
|
||||
conn_tracker,
|
||||
@@ -161,8 +161,9 @@ impl TcpListenerManager {
|
||||
}
|
||||
|
||||
/// Set TLS certificate configurations.
|
||||
pub fn set_tls_configs(&mut self, configs: HashMap<String, TlsCertConfig>) {
|
||||
self.tls_configs = Arc::new(configs);
|
||||
/// Uses ArcSwap so running accept loops immediately see the new certs.
|
||||
pub fn set_tls_configs(&self, configs: HashMap<String, TlsCertConfig>) {
|
||||
self.tls_configs.store(Arc::new(configs));
|
||||
}
|
||||
|
||||
/// Set the shared socket-handler relay path.
|
||||
@@ -284,7 +285,7 @@ impl TcpListenerManager {
|
||||
port: u16,
|
||||
route_manager_swap: Arc<ArcSwap<RouteManager>>,
|
||||
metrics: Arc<MetricsCollector>,
|
||||
tls_configs: Arc<HashMap<String, TlsCertConfig>>,
|
||||
tls_configs: Arc<ArcSwap<HashMap<String, TlsCertConfig>>>,
|
||||
http_proxy: Arc<HttpProxyService>,
|
||||
conn_config: Arc<ConnectionConfig>,
|
||||
conn_tracker: Arc<ConnectionTracker>,
|
||||
@@ -314,7 +315,8 @@ impl TcpListenerManager {
|
||||
// Load the latest route manager from ArcSwap on each connection
|
||||
let rm = route_manager_swap.load_full();
|
||||
let m = Arc::clone(&metrics);
|
||||
let tc = Arc::clone(&tls_configs);
|
||||
// Load the latest TLS configs from ArcSwap on each connection
|
||||
let tc = tls_configs.load_full();
|
||||
let hp = Arc::clone(&http_proxy);
|
||||
let cc = Arc::clone(&conn_config);
|
||||
let ct = Arc::clone(&conn_tracker);
|
||||
|
||||
@@ -15,8 +15,6 @@ tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
rcgen = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
//! ACME (Let's Encrypt) integration using instant-acme.
|
||||
//!
|
||||
//! This module handles HTTP-01 challenge creation and certificate provisioning.
|
||||
//! Supports persisting ACME account credentials to disk for reuse across restarts.
|
||||
//! Account credentials are ephemeral — the consumer owns all persistence.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use instant_acme::{
|
||||
Account, NewAccount, NewOrder, Identifier, ChallengeType, OrderStatus,
|
||||
AccountCredentials,
|
||||
};
|
||||
use rcgen::{CertificateParams, KeyPair};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{debug, info};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AcmeError {
|
||||
@@ -26,8 +25,6 @@ pub enum AcmeError {
|
||||
NoHttp01Challenge,
|
||||
#[error("Timeout waiting for order: {0}")]
|
||||
Timeout(String),
|
||||
#[error("Account persistence error: {0}")]
|
||||
Persistence(String),
|
||||
}
|
||||
|
||||
/// Pending HTTP-01 challenge that needs to be served.
|
||||
@@ -41,8 +38,6 @@ pub struct PendingChallenge {
|
||||
pub struct AcmeClient {
|
||||
use_production: bool,
|
||||
email: String,
|
||||
/// Optional directory where account.json is persisted.
|
||||
account_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl AcmeClient {
|
||||
@@ -50,56 +45,15 @@ impl AcmeClient {
|
||||
Self {
|
||||
use_production,
|
||||
email,
|
||||
account_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new client with account persistence at the given directory.
|
||||
pub fn with_persistence(email: String, use_production: bool, account_dir: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
use_production,
|
||||
email,
|
||||
account_dir: Some(account_dir.as_ref().to_path_buf()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get or create an ACME account, persisting credentials if account_dir is set.
|
||||
/// Create a new ACME account (ephemeral — not persisted).
|
||||
async fn get_or_create_account(&self) -> Result<Account, AcmeError> {
|
||||
let directory_url = self.directory_url();
|
||||
|
||||
// Try to restore from persisted credentials
|
||||
if let Some(ref dir) = self.account_dir {
|
||||
let account_file = dir.join("account.json");
|
||||
if account_file.exists() {
|
||||
match std::fs::read_to_string(&account_file) {
|
||||
Ok(json) => {
|
||||
match serde_json::from_str::<AccountCredentials>(&json) {
|
||||
Ok(credentials) => {
|
||||
match Account::from_credentials(credentials).await {
|
||||
Ok(account) => {
|
||||
debug!("Restored ACME account from {}", account_file.display());
|
||||
return Ok(account);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to restore ACME account, creating new: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Invalid account.json, creating new account: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not read account.json: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new account
|
||||
let contact = format!("mailto:{}", self.email);
|
||||
let (account, credentials) = Account::create(
|
||||
let (account, _credentials) = Account::create(
|
||||
&NewAccount {
|
||||
contact: &[&contact],
|
||||
terms_of_service_agreed: true,
|
||||
@@ -113,27 +67,6 @@ impl AcmeClient {
|
||||
|
||||
debug!("ACME account created");
|
||||
|
||||
// Persist credentials if we have a directory
|
||||
if let Some(ref dir) = self.account_dir {
|
||||
if let Err(e) = std::fs::create_dir_all(dir) {
|
||||
warn!("Failed to create account directory {}: {}", dir.display(), e);
|
||||
} else {
|
||||
let account_file = dir.join("account.json");
|
||||
match serde_json::to_string_pretty(&credentials) {
|
||||
Ok(json) => {
|
||||
if let Err(e) = std::fs::write(&account_file, &json) {
|
||||
warn!("Failed to persist ACME account to {}: {}", account_file.display(), e);
|
||||
} else {
|
||||
info!("ACME account credentials persisted to {}", account_file.display());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to serialize account credentials: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
@@ -158,7 +91,7 @@ impl AcmeClient {
|
||||
{
|
||||
info!("Starting ACME provisioning for {} via {}", domain, self.directory_url());
|
||||
|
||||
// 1. Get or create ACME account (with persistence)
|
||||
// 1. Get or create ACME account
|
||||
let account = self.get_or_create_account().await?;
|
||||
|
||||
// 2. Create order
|
||||
@@ -339,22 +272,4 @@ mod tests {
|
||||
assert!(!client.directory_url().contains("staging"));
|
||||
assert!(client.is_production());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_persistence_sets_account_dir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let client = AcmeClient::with_persistence(
|
||||
"test@example.com".to_string(),
|
||||
false,
|
||||
tmp.path(),
|
||||
);
|
||||
assert!(client.account_dir.is_some());
|
||||
assert_eq!(client.account_dir.unwrap(), tmp.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_without_persistence_no_account_dir() {
|
||||
let client = AcmeClient::new("test@example.com".to_string(), false);
|
||||
assert!(client.account_dir.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ use crate::acme::AcmeClient;
|
||||
pub enum CertManagerError {
|
||||
#[error("ACME provisioning failed for {domain}: {message}")]
|
||||
AcmeFailure { domain: String, message: String },
|
||||
#[error("Certificate store error: {0}")]
|
||||
Store(#[from] crate::cert_store::CertStoreError),
|
||||
#[error("No ACME email configured")]
|
||||
NoEmail,
|
||||
}
|
||||
@@ -46,25 +44,19 @@ impl CertManager {
|
||||
|
||||
/// Create an ACME client using this manager's configuration.
|
||||
/// Returns None if no ACME email is configured.
|
||||
/// Account credentials are persisted in the cert store base directory.
|
||||
pub fn acme_client(&self) -> Option<AcmeClient> {
|
||||
self.acme_email.as_ref().map(|email| {
|
||||
AcmeClient::with_persistence(
|
||||
email.clone(),
|
||||
self.use_production,
|
||||
self.store.base_dir(),
|
||||
)
|
||||
AcmeClient::new(email.clone(), self.use_production)
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a static certificate into the store.
|
||||
/// Load a static certificate into the store (infallible — pure cache insert).
|
||||
pub fn load_static(
|
||||
&mut self,
|
||||
domain: String,
|
||||
bundle: CertBundle,
|
||||
) -> Result<(), CertManagerError> {
|
||||
self.store.store(domain, bundle)?;
|
||||
Ok(())
|
||||
) {
|
||||
self.store.store(domain, bundle);
|
||||
}
|
||||
|
||||
/// Check and return domains that need certificate renewal.
|
||||
@@ -153,19 +145,12 @@ impl CertManager {
|
||||
},
|
||||
};
|
||||
|
||||
self.store.store(domain.to_string(), bundle.clone())?;
|
||||
self.store.store(domain.to_string(), bundle.clone());
|
||||
info!("Certificate renewed and stored for {}", domain);
|
||||
|
||||
Ok(bundle)
|
||||
}
|
||||
|
||||
/// Load all certificates from disk.
|
||||
pub fn load_all(&mut self) -> Result<usize, CertManagerError> {
|
||||
let loaded = self.store.load_all()?;
|
||||
info!("Loaded {} certificates from store", loaded);
|
||||
Ok(loaded)
|
||||
}
|
||||
|
||||
/// Whether this manager has an ACME email configured.
|
||||
pub fn has_acme(&self) -> bool {
|
||||
self.acme_email.is_some()
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CertStoreError {
|
||||
#[error("Certificate not found for domain: {0}")]
|
||||
NotFound(String),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Invalid certificate: {0}")]
|
||||
Invalid(String),
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Certificate metadata stored alongside certs on disk.
|
||||
/// Certificate metadata stored alongside certs.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CertMetadata {
|
||||
@@ -45,27 +31,18 @@ pub struct CertBundle {
|
||||
pub metadata: CertMetadata,
|
||||
}
|
||||
|
||||
/// Filesystem-backed certificate store.
|
||||
/// In-memory certificate store.
|
||||
///
|
||||
/// File layout per domain:
|
||||
/// ```text
|
||||
/// {base_dir}/{domain}/
|
||||
/// key.pem
|
||||
/// cert.pem
|
||||
/// ca.pem (optional)
|
||||
/// metadata.json
|
||||
/// ```
|
||||
/// All persistence is owned by the consumer (TypeScript side).
|
||||
/// This struct is a thin HashMap wrapper used as a runtime cache.
|
||||
pub struct CertStore {
|
||||
base_dir: PathBuf,
|
||||
/// In-memory cache of loaded certs
|
||||
cache: HashMap<String, CertBundle>,
|
||||
}
|
||||
|
||||
impl CertStore {
|
||||
/// Create a new cert store at the given directory.
|
||||
pub fn new(base_dir: impl AsRef<Path>) -> Self {
|
||||
/// Create a new empty cert store.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_dir: base_dir.as_ref().to_path_buf(),
|
||||
cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
@@ -75,33 +52,9 @@ impl CertStore {
|
||||
self.cache.get(domain)
|
||||
}
|
||||
|
||||
/// Store a certificate to both cache and filesystem.
|
||||
pub fn store(&mut self, domain: String, bundle: CertBundle) -> Result<(), CertStoreError> {
|
||||
// Sanitize domain for directory name (replace wildcards)
|
||||
let dir_name = domain.replace('*', "_wildcard_");
|
||||
let cert_dir = self.base_dir.join(&dir_name);
|
||||
|
||||
// Create directory
|
||||
std::fs::create_dir_all(&cert_dir)?;
|
||||
|
||||
// Write key
|
||||
std::fs::write(cert_dir.join("key.pem"), &bundle.key_pem)?;
|
||||
|
||||
// Write cert
|
||||
std::fs::write(cert_dir.join("cert.pem"), &bundle.cert_pem)?;
|
||||
|
||||
// Write CA cert if present
|
||||
if let Some(ref ca) = bundle.ca_pem {
|
||||
std::fs::write(cert_dir.join("ca.pem"), ca)?;
|
||||
}
|
||||
|
||||
// Write metadata
|
||||
let metadata_json = serde_json::to_string_pretty(&bundle.metadata)?;
|
||||
std::fs::write(cert_dir.join("metadata.json"), metadata_json)?;
|
||||
|
||||
// Update cache
|
||||
/// Store a certificate in the cache.
|
||||
pub fn store(&mut self, domain: String, bundle: CertBundle) {
|
||||
self.cache.insert(domain, bundle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a certificate exists for a domain.
|
||||
@@ -109,68 +62,6 @@ impl CertStore {
|
||||
self.cache.contains_key(domain)
|
||||
}
|
||||
|
||||
/// Load all certificates from the base directory.
|
||||
pub fn load_all(&mut self) -> Result<usize, CertStoreError> {
|
||||
if !self.base_dir.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let entries = std::fs::read_dir(&self.base_dir)?;
|
||||
let mut loaded = 0;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let metadata_path = path.join("metadata.json");
|
||||
let key_path = path.join("key.pem");
|
||||
let cert_path = path.join("cert.pem");
|
||||
|
||||
// All three files must exist
|
||||
if !metadata_path.exists() || !key_path.exists() || !cert_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load metadata
|
||||
let metadata_str = std::fs::read_to_string(&metadata_path)?;
|
||||
let metadata: CertMetadata = serde_json::from_str(&metadata_str)?;
|
||||
|
||||
// Load key and cert
|
||||
let key_pem = std::fs::read_to_string(&key_path)?;
|
||||
let cert_pem = std::fs::read_to_string(&cert_path)?;
|
||||
|
||||
// Load CA cert if present
|
||||
let ca_path = path.join("ca.pem");
|
||||
let ca_pem = if ca_path.exists() {
|
||||
Some(std::fs::read_to_string(&ca_path)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let domain = metadata.domain.clone();
|
||||
let bundle = CertBundle {
|
||||
key_pem,
|
||||
cert_pem,
|
||||
ca_pem,
|
||||
metadata,
|
||||
};
|
||||
|
||||
self.cache.insert(domain, bundle);
|
||||
loaded += 1;
|
||||
}
|
||||
|
||||
Ok(loaded)
|
||||
}
|
||||
|
||||
/// Get the base directory.
|
||||
pub fn base_dir(&self) -> &Path {
|
||||
&self.base_dir
|
||||
}
|
||||
|
||||
/// Get the number of cached certificates.
|
||||
pub fn count(&self) -> usize {
|
||||
self.cache.len()
|
||||
@@ -181,17 +72,15 @@ impl CertStore {
|
||||
self.cache.iter()
|
||||
}
|
||||
|
||||
/// Remove a certificate from cache and filesystem.
|
||||
pub fn remove(&mut self, domain: &str) -> Result<bool, CertStoreError> {
|
||||
let removed = self.cache.remove(domain).is_some();
|
||||
if removed {
|
||||
let dir_name = domain.replace('*', "_wildcard_");
|
||||
let cert_dir = self.base_dir.join(&dir_name);
|
||||
if cert_dir.exists() {
|
||||
std::fs::remove_dir_all(&cert_dir)?;
|
||||
}
|
||||
}
|
||||
Ok(removed)
|
||||
/// Remove a certificate from the cache.
|
||||
pub fn remove(&mut self, domain: &str) -> bool {
|
||||
self.cache.remove(domain).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CertStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,100 +104,71 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_store_and_load_roundtrip() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let mut store = CertStore::new(tmp.path());
|
||||
fn test_store_and_get() {
|
||||
let mut store = CertStore::new();
|
||||
|
||||
let bundle = make_test_bundle("example.com");
|
||||
store.store("example.com".to_string(), bundle.clone()).unwrap();
|
||||
store.store("example.com".to_string(), bundle.clone());
|
||||
|
||||
// Verify files exist
|
||||
let cert_dir = tmp.path().join("example.com");
|
||||
assert!(cert_dir.join("key.pem").exists());
|
||||
assert!(cert_dir.join("cert.pem").exists());
|
||||
assert!(cert_dir.join("metadata.json").exists());
|
||||
assert!(!cert_dir.join("ca.pem").exists()); // No CA cert
|
||||
|
||||
// Load into a fresh store
|
||||
let mut store2 = CertStore::new(tmp.path());
|
||||
let loaded = store2.load_all().unwrap();
|
||||
assert_eq!(loaded, 1);
|
||||
|
||||
let loaded_bundle = store2.get("example.com").unwrap();
|
||||
assert_eq!(loaded_bundle.key_pem, bundle.key_pem);
|
||||
assert_eq!(loaded_bundle.cert_pem, bundle.cert_pem);
|
||||
assert_eq!(loaded_bundle.metadata.domain, "example.com");
|
||||
assert_eq!(loaded_bundle.metadata.source, CertSource::Static);
|
||||
let loaded = store.get("example.com").unwrap();
|
||||
assert_eq!(loaded.key_pem, bundle.key_pem);
|
||||
assert_eq!(loaded.cert_pem, bundle.cert_pem);
|
||||
assert_eq!(loaded.metadata.domain, "example.com");
|
||||
assert_eq!(loaded.metadata.source, CertSource::Static);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_store_with_ca_cert() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let mut store = CertStore::new(tmp.path());
|
||||
let mut store = CertStore::new();
|
||||
|
||||
let mut bundle = make_test_bundle("secure.com");
|
||||
bundle.ca_pem = Some("-----BEGIN CERTIFICATE-----\nca-cert\n-----END CERTIFICATE-----\n".to_string());
|
||||
store.store("secure.com".to_string(), bundle).unwrap();
|
||||
store.store("secure.com".to_string(), bundle);
|
||||
|
||||
let cert_dir = tmp.path().join("secure.com");
|
||||
assert!(cert_dir.join("ca.pem").exists());
|
||||
|
||||
let mut store2 = CertStore::new(tmp.path());
|
||||
store2.load_all().unwrap();
|
||||
let loaded = store2.get("secure.com").unwrap();
|
||||
let loaded = store.get("secure.com").unwrap();
|
||||
assert!(loaded.ca_pem.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_all_multiple_certs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let mut store = CertStore::new(tmp.path());
|
||||
fn test_multiple_certs() {
|
||||
let mut store = CertStore::new();
|
||||
|
||||
store.store("a.com".to_string(), make_test_bundle("a.com")).unwrap();
|
||||
store.store("b.com".to_string(), make_test_bundle("b.com")).unwrap();
|
||||
store.store("c.com".to_string(), make_test_bundle("c.com")).unwrap();
|
||||
store.store("a.com".to_string(), make_test_bundle("a.com"));
|
||||
store.store("b.com".to_string(), make_test_bundle("b.com"));
|
||||
store.store("c.com".to_string(), make_test_bundle("c.com"));
|
||||
|
||||
let mut store2 = CertStore::new(tmp.path());
|
||||
let loaded = store2.load_all().unwrap();
|
||||
assert_eq!(loaded, 3);
|
||||
assert!(store2.has("a.com"));
|
||||
assert!(store2.has("b.com"));
|
||||
assert!(store2.has("c.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_all_missing_directory() {
|
||||
let mut store = CertStore::new("/nonexistent/path/to/certs");
|
||||
let loaded = store.load_all().unwrap();
|
||||
assert_eq!(loaded, 0);
|
||||
assert_eq!(store.count(), 3);
|
||||
assert!(store.has("a.com"));
|
||||
assert!(store.has("b.com"));
|
||||
assert!(store.has("c.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_cert() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let mut store = CertStore::new(tmp.path());
|
||||
let mut store = CertStore::new();
|
||||
|
||||
store.store("remove-me.com".to_string(), make_test_bundle("remove-me.com")).unwrap();
|
||||
store.store("remove-me.com".to_string(), make_test_bundle("remove-me.com"));
|
||||
assert!(store.has("remove-me.com"));
|
||||
|
||||
let removed = store.remove("remove-me.com").unwrap();
|
||||
let removed = store.remove("remove-me.com");
|
||||
assert!(removed);
|
||||
assert!(!store.has("remove-me.com"));
|
||||
assert!(!tmp.path().join("remove-me.com").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wildcard_domain_storage() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let mut store = CertStore::new(tmp.path());
|
||||
fn test_remove_nonexistent() {
|
||||
let mut store = CertStore::new();
|
||||
assert!(!store.remove("nonexistent.com"));
|
||||
}
|
||||
|
||||
store.store("*.example.com".to_string(), make_test_bundle("*.example.com")).unwrap();
|
||||
#[test]
|
||||
fn test_wildcard_domain() {
|
||||
let mut store = CertStore::new();
|
||||
|
||||
// Directory should use sanitized name
|
||||
assert!(tmp.path().join("_wildcard_.example.com").exists());
|
||||
store.store("*.example.com".to_string(), make_test_bundle("*.example.com"));
|
||||
assert!(store.has("*.example.com"));
|
||||
|
||||
let mut store2 = CertStore::new(tmp.path());
|
||||
store2.load_all().unwrap();
|
||||
assert!(store2.has("*.example.com"));
|
||||
let loaded = store.get("*.example.com").unwrap();
|
||||
assert_eq!(loaded.metadata.domain, "*.example.com");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,15 +184,12 @@ impl RustProxy {
|
||||
return None;
|
||||
}
|
||||
|
||||
let store_path = acme.certificate_store
|
||||
.as_deref()
|
||||
.unwrap_or("./certs");
|
||||
let email = acme.email.clone()
|
||||
.or_else(|| acme.account_email.clone());
|
||||
let use_production = acme.use_production.unwrap_or(false);
|
||||
let renew_before_days = acme.renew_threshold_days.unwrap_or(30);
|
||||
|
||||
let store = CertStore::new(store_path);
|
||||
let store = CertStore::new();
|
||||
Some(CertManager::new(store, email, use_production, renew_before_days))
|
||||
}
|
||||
|
||||
@@ -222,19 +219,6 @@ impl RustProxy {
|
||||
|
||||
info!("Starting RustProxy...");
|
||||
|
||||
// Load persisted certificates
|
||||
if let Some(ref cm) = self.cert_manager {
|
||||
let mut cm = cm.lock().await;
|
||||
match cm.load_all() {
|
||||
Ok(count) => {
|
||||
if count > 0 {
|
||||
info!("Loaded {} persisted certificates", count);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("Failed to load persisted certificates: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-provision certificates for routes with certificate: 'auto'
|
||||
self.auto_provision_certificates().await;
|
||||
|
||||
@@ -396,9 +380,7 @@ impl RustProxy {
|
||||
};
|
||||
|
||||
let mut cm = cm_arc.lock().await;
|
||||
if let Err(e) = cm.load_static(domain.clone(), bundle) {
|
||||
error!("Failed to store certificate for {}: {}", domain, e);
|
||||
}
|
||||
cm.load_static(domain.clone(), bundle);
|
||||
|
||||
info!("Certificate provisioned for {}", domain);
|
||||
}
|
||||
@@ -775,8 +757,7 @@ impl RustProxy {
|
||||
};
|
||||
|
||||
let mut cm = cm_arc.lock().await;
|
||||
cm.load_static(domain.to_string(), bundle)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to store certificate: {}", e))?;
|
||||
cm.load_static(domain.to_string(), bundle);
|
||||
}
|
||||
|
||||
// Hot-swap TLS config on the listener
|
||||
|
||||
Reference in New Issue
Block a user