use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};
use lru::LruCache;
use crate::error::ProxyError;
use crate::tls::ca::{CaStore, CertifiedKey};
pub struct CertCache {
inner: Mutex<LruCache<String, Arc<CertifiedKey>>>,
}
impl CertCache {
pub fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(LruCache::new(
NonZeroUsize::new(capacity).expect("cert cache capacity must be non-zero"),
)),
}
}
pub fn get_or_insert(&self, domain: &str, ca: &CaStore) -> Result<Arc<CertifiedKey>, ProxyError> {
let mut cache = self.inner.lock().expect("cert cache lock poisoned");
if let Some(ck) = cache.get(domain) {
return Ok(Arc::clone(ck));
}
let ck = Arc::new(ca.sign_cert(domain)?);
cache.put(domain.to_string(), Arc::clone(&ck));
Ok(ck)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn get_or_insert_returns_cert_on_cache_miss() {
let dir = TempDir::new().unwrap();
let ca = CaStore::load_or_create(dir.path()).await.unwrap();
let cache = CertCache::new(10);
let ck = cache.get_or_insert("api.openai.com", &ca).unwrap();
assert!(!ck.cert_der.is_empty());
}
#[tokio::test]
async fn get_or_insert_returns_same_arc_on_cache_hit() {
let dir = TempDir::new().unwrap();
let ca = CaStore::load_or_create(dir.path()).await.unwrap();
let cache = CertCache::new(10);
let ck1 = cache.get_or_insert("api.openai.com", &ca).unwrap();
let ck2 = cache.get_or_insert("api.openai.com", &ca).unwrap();
assert!(Arc::ptr_eq(&ck1, &ck2), "second call must return the cached Arc");
}
#[tokio::test]
async fn get_or_insert_different_domains_get_different_certs() {
let dir = TempDir::new().unwrap();
let ca = CaStore::load_or_create(dir.path()).await.unwrap();
let cache = CertCache::new(10);
let ck1 = cache.get_or_insert("api.openai.com", &ca).unwrap();
let ck2 = cache.get_or_insert("api.anthropic.com", &ca).unwrap();
assert!(!Arc::ptr_eq(&ck1, &ck2));
assert_ne!(ck1.cert_der, ck2.cert_der);
}
}