use crate::config::AuthCacheConfig;
use lru::LruCache;
use rsipstack::transport::SipAddr;
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
use tracing::{debug, trace};
pub type AuthCacheKey = (String, String);
#[derive(Clone, Debug)]
struct AuthCacheEntry {
source_addr: String,
authenticated_at: Instant,
ttl: Duration,
}
impl AuthCacheEntry {
fn is_expired(&self) -> bool {
Instant::now().duration_since(self.authenticated_at) > self.ttl
}
}
#[derive(Clone)]
pub struct DialogAuthCache {
inner: Arc<Mutex<LruCache<AuthCacheKey, AuthCacheEntry>>>,
ttl: Duration,
}
impl DialogAuthCache {
pub fn new(config: &AuthCacheConfig) -> Self {
let cache_size = NonZeroUsize::new(config.cache_size)
.unwrap_or_else(|| NonZeroUsize::new(10000).unwrap());
Self {
inner: Arc::new(Mutex::new(LruCache::new(cache_size))),
ttl: Duration::from_secs(config.ttl_seconds),
}
}
pub async fn put(&self, key: AuthCacheKey, source_addr: SipAddr) {
let entry = AuthCacheEntry {
source_addr: source_addr.to_string(),
authenticated_at: Instant::now(),
ttl: self.ttl,
};
let mut cache = self.inner.lock().await;
cache.put(key.clone(), entry);
debug!(
call_id = %key.0,
from_tag = %key.1,
source_addr = %source_addr,
"Cached authenticated dialog"
);
}
pub async fn is_authenticated(&self, key: &AuthCacheKey, source_addr: &SipAddr) -> bool {
let mut cache = self.inner.lock().await;
if let Some(entry) = cache.get(key) {
if entry.is_expired() {
trace!(
call_id = %key.0,
from_tag = %key.1,
"Dialog auth cache entry expired"
);
return false;
}
let source_str = source_addr.to_string();
if entry.source_addr == source_str {
trace!(
call_id = %key.0,
from_tag = %key.1,
source_addr = %source_addr,
"Dialog auth cache hit, skipping authentication"
);
return true;
}
trace!(
call_id = %key.0,
from_tag = %key.1,
cached_addr = %entry.source_addr,
current_addr = %source_addr,
"Dialog auth cache source address mismatch"
);
}
false
}
pub async fn remove(&self, key: &AuthCacheKey) {
let mut cache = self.inner.lock().await;
cache.pop(key);
}
pub async fn len(&self) -> usize {
let cache = self.inner.lock().await;
cache.len()
}
pub async fn is_empty(&self) -> bool {
let cache = self.inner.lock().await;
cache.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rsipstack::sip::{HostWithPort, Transport};
use std::time::Duration;
fn create_test_config() -> AuthCacheConfig {
AuthCacheConfig {
enabled: true,
cache_size: 100,
ttl_seconds: 3600,
}
}
fn create_test_key() -> AuthCacheKey {
("test-call-id".to_string(), "from-tag".to_string())
}
fn create_test_sip_addr(host: &str, port: u16) -> SipAddr {
SipAddr::new(
Transport::Udp,
HostWithPort::try_from(format!("{}:{}", host, port)).unwrap(),
)
}
#[tokio::test]
async fn test_cache_hit() {
let cache = DialogAuthCache::new(&create_test_config());
let key = create_test_key();
let source_addr = create_test_sip_addr("192.168.1.100", 5060);
cache.put(key.clone(), source_addr.clone()).await;
assert!(
cache.is_authenticated(&key, &source_addr).await,
"Should authenticate cached dialog with matching source"
);
}
#[tokio::test]
async fn test_cache_miss_different_source() {
let cache = DialogAuthCache::new(&create_test_config());
let key = create_test_key();
let source_addr1 = create_test_sip_addr("192.168.1.100", 5060);
let source_addr2 = create_test_sip_addr("192.168.1.101", 5060);
cache.put(key.clone(), source_addr1).await;
assert!(
!cache.is_authenticated(&key, &source_addr2).await,
"Should not authenticate with different source address"
);
}
#[tokio::test]
async fn test_cache_miss_unknown_dialog() {
let cache = DialogAuthCache::new(&create_test_config());
let key = create_test_key();
let source_addr = create_test_sip_addr("192.168.1.100", 5060);
assert!(
!cache.is_authenticated(&key, &source_addr).await,
"Should not authenticate unknown dialog"
);
}
#[tokio::test]
async fn test_cache_ttl_expiration() {
let mut config = create_test_config();
config.ttl_seconds = 1;
let cache = DialogAuthCache::new(&config);
let key = create_test_key();
let source_addr = create_test_sip_addr("192.168.1.100", 5060);
cache.put(key.clone(), source_addr.clone()).await;
assert!(
cache.is_authenticated(&key, &source_addr).await,
"Should authenticate before TTL expires"
);
tokio::time::sleep(Duration::from_secs(2)).await;
assert!(
!cache.is_authenticated(&key, &source_addr).await,
"Should not authenticate after TTL expires"
);
}
#[tokio::test]
async fn test_cache_lru_eviction() {
let mut config = create_test_config();
config.cache_size = 2;
let cache = DialogAuthCache::new(&config);
let source_addr = create_test_sip_addr("192.168.1.100", 5060);
let key1: AuthCacheKey = ("call-1".to_string(), "tag-1".to_string());
let key2: AuthCacheKey = ("call-2".to_string(), "tag-2".to_string());
let key3: AuthCacheKey = ("call-3".to_string(), "tag-3".to_string());
cache.put(key1.clone(), source_addr.clone()).await;
cache.put(key2.clone(), source_addr.clone()).await;
cache.put(key3.clone(), source_addr.clone()).await;
assert!(
!cache.is_authenticated(&key1, &source_addr).await,
"Oldest entry should be evicted"
);
assert!(
cache.is_authenticated(&key2, &source_addr).await,
"Recent entry should still be cached"
);
assert!(
cache.is_authenticated(&key3, &source_addr).await,
"Newest entry should still be cached"
);
}
#[tokio::test]
async fn test_cache_remove() {
let cache = DialogAuthCache::new(&create_test_config());
let key = create_test_key();
let source_addr = create_test_sip_addr("192.168.1.100", 5060);
cache.put(key.clone(), source_addr.clone()).await;
assert!(cache.is_authenticated(&key, &source_addr).await);
cache.remove(&key).await;
assert!(!cache.is_authenticated(&key, &source_addr).await);
}
}