use crate::error::Result;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Debug, Clone)]
pub struct NonceManager {
nonce_url: String,
http_client: reqwest::Client,
nonce_pool: Arc<Mutex<Vec<String>>>,
}
impl NonceManager {
pub fn new(new_nonce_url: impl Into<String>, http_client: reqwest::Client) -> Self {
let url = new_nonce_url.into();
tracing::debug!("Initializing NonceManager with URL: {}", url);
Self {
nonce_url: url,
http_client,
nonce_pool: Arc::new(Mutex::new(Vec::new())),
}
}
pub async fn get_nonce(&self) -> Result<String> {
{
let mut pool = self.nonce_pool.lock().await;
if let Some(nonce) = pool.pop() {
tracing::debug!("Using cached nonce from pool (remaining: {})", pool.len());
return Ok(nonce);
}
}
tracing::debug!("Nonce pool empty, fetching fresh nonce from server");
self.fetch_nonce().await
}
async fn fetch_nonce(&self) -> Result<String> {
tracing::info!("Fetching new nonce from: {}", self.nonce_url);
let response = self
.http_client
.head(&self.nonce_url)
.send()
.await
.map_err(|e| {
tracing::error!("Network error while fetching nonce: {}", e);
crate::error::AcmeError::transport(format!("Failed to fetch nonce: {}", e))
})?;
if !response.status().is_success() {
tracing::error!(
"Failed to fetch nonce, server returned status: {}",
response.status()
);
return Err(crate::error::AcmeError::protocol(format!(
"Failed to fetch nonce: HTTP {}",
response.status()
)));
}
let nonce = response
.headers()
.get("replay-nonce")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
.ok_or_else(|| {
tracing::error!("Server response missing 'replay-nonce' header");
crate::error::AcmeError::protocol("Missing replay-nonce header".to_string())
})?;
tracing::debug!("Successfully fetched new nonce");
Ok(nonce)
}
pub async fn cache_nonce(&self, nonce: String) {
let mut pool = self.nonce_pool.lock().await;
tracing::debug!("Caching nonce (pool size before: {})", pool.len());
pool.push(nonce);
}
pub async fn clear_pool(&self) {
tracing::debug!("Clearing nonce pool");
let mut pool = self.nonce_pool.lock().await;
pool.clear();
}
pub async fn pool_size(&self) -> usize {
let pool = self.nonce_pool.lock().await;
pool.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_nonce_manager_creation() {
let manager =
NonceManager::new("https://example.com/acme/new-nonce", reqwest::Client::new());
assert_eq!(manager.pool_size().await, 0);
}
#[tokio::test]
async fn test_cache_nonce() {
let manager =
NonceManager::new("https://example.com/acme/new-nonce", reqwest::Client::new());
manager.cache_nonce("test-nonce-123".to_string()).await;
assert_eq!(manager.pool_size().await, 1);
let nonce = manager.get_nonce().await;
assert!(nonce.is_ok());
assert_eq!(nonce.unwrap(), "test-nonce-123");
assert_eq!(manager.pool_size().await, 0);
}
#[tokio::test]
async fn test_clear_pool() {
let manager =
NonceManager::new("https://example.com/acme/new-nonce", reqwest::Client::new());
manager.cache_nonce("nonce-1".to_string()).await;
manager.cache_nonce("nonce-2".to_string()).await;
assert_eq!(manager.pool_size().await, 2);
manager.clear_pool().await;
assert_eq!(manager.pool_size().await, 0);
}
}