use crate::bootstrap_cache::BootstrapCache;
use crate::nat_traversal_api::PeerId;
use crate::token::TokenStore;
use bytes::Bytes;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use tracing::{debug, warn};
#[derive(Debug)]
pub struct BootstrapTokenStore {
cache: Arc<BootstrapCache>,
local_cache: Arc<RwLock<HashMap<String, Vec<u8>>>>,
}
impl BootstrapTokenStore {
pub async fn new(cache: Arc<BootstrapCache>) -> Self {
let tokens = cache.get_all_tokens().await;
let mut local = HashMap::new();
for (peer_id, token) in tokens {
let key = hex::encode(peer_id.0);
local.insert(key, token);
}
debug!(
"Initialized BootstrapTokenStore with {} tokens",
local.len()
);
Self {
cache,
local_cache: Arc::new(RwLock::new(local)),
}
}
}
impl TokenStore for BootstrapTokenStore {
fn insert(&self, server_name: &str, token: Bytes) {
let token_vec = token.to_vec();
if let Ok(mut local) = self.local_cache.write() {
local.insert(server_name.to_string(), token_vec.clone());
} else {
warn!("Failed to acquire write lock on local token cache");
}
if let Ok(bytes) = hex::decode(server_name) {
if let Ok(arr) = <[u8; 32]>::try_from(bytes) {
let peer_id = PeerId(arr);
let cache = self.cache.clone();
let token_clone = token_vec;
tokio::spawn(async move {
cache.update_token(peer_id, token_clone).await;
});
return;
}
}
debug!(
"Received token for non-PeerId server name: {}, not persisting to disk",
server_name
);
}
fn take(&self, server_name: &str) -> Option<Bytes> {
if let Ok(mut local) = self.local_cache.write() {
local.remove(server_name).map(Bytes::from)
} else {
warn!("Failed to acquire write lock on local token cache");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bootstrap_cache::BootstrapCacheConfig;
use tempfile::TempDir;
async fn create_test_cache(temp_dir: &TempDir) -> Arc<BootstrapCache> {
let config = BootstrapCacheConfig::builder()
.cache_dir(temp_dir.path())
.max_peers(100)
.epsilon(0.0)
.min_peers_to_save(1)
.build();
Arc::new(
BootstrapCache::open(config)
.await
.expect("Failed to create cache"),
)
}
#[tokio::test]
async fn insert_and_take_valid_peer_id() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let cache = create_test_cache(&temp_dir).await;
let store = BootstrapTokenStore::new(cache).await;
let peer_id_hex = hex::encode([0xAB; 32]);
let token = Bytes::from_static(b"test_token_data");
store.insert(&peer_id_hex, token.clone());
let taken = store.take(&peer_id_hex);
assert!(taken.is_some(), "First take should return token");
assert_eq!(taken.expect("should have token"), token);
let taken_again = store.take(&peer_id_hex);
assert!(
taken_again.is_none(),
"Second take should return None (one-shot)"
);
}
#[tokio::test]
async fn take_nonexistent_returns_none() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let cache = create_test_cache(&temp_dir).await;
let store = BootstrapTokenStore::new(cache).await;
let result = store.take("nonexistent_key");
assert!(result.is_none());
}
#[tokio::test]
async fn insert_non_peer_id_server_name() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let cache = create_test_cache(&temp_dir).await;
let store = BootstrapTokenStore::new(cache).await;
let test_cases = ["192.168.1.1:8000", "server.example.com", "localhost", "::1"];
for server_name in test_cases {
let token = Bytes::from(format!("token_for_{}", server_name));
store.insert(server_name, token.clone());
let taken = store.take(server_name);
assert!(
taken.is_some(),
"Should be able to take token for {}",
server_name
);
assert_eq!(taken.expect("should have token"), token);
}
}
#[tokio::test]
async fn hex_decode_edge_cases() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let cache = create_test_cache(&temp_dir).await;
let store = BootstrapTokenStore::new(cache).await;
let edge_cases = [
"", "abc", "gggg", "00112233", &hex::encode([0xFF; 16]), ];
for server_name in edge_cases {
let token = Bytes::from_static(b"edge_case_token");
store.insert(server_name, token.clone());
let taken = store.take(server_name);
assert!(
taken.is_some(),
"Should take token for edge case: '{}'",
server_name
);
}
}
#[tokio::test]
async fn multiple_tokens_different_peers() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let cache = create_test_cache(&temp_dir).await;
let store = BootstrapTokenStore::new(cache).await;
let peer1 = hex::encode([0x11; 32]);
let peer2 = hex::encode([0x22; 32]);
let peer3 = hex::encode([0x33; 32]);
store.insert(&peer1, Bytes::from_static(b"token1"));
store.insert(&peer2, Bytes::from_static(b"token2"));
store.insert(&peer3, Bytes::from_static(b"token3"));
assert_eq!(store.take(&peer1), Some(Bytes::from_static(b"token1")));
assert_eq!(store.take(&peer2), Some(Bytes::from_static(b"token2")));
assert_eq!(store.take(&peer3), Some(Bytes::from_static(b"token3")));
assert!(store.take(&peer1).is_none());
assert!(store.take(&peer2).is_none());
assert!(store.take(&peer3).is_none());
}
#[tokio::test]
async fn overwrite_token_for_same_peer() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let cache = create_test_cache(&temp_dir).await;
let store = BootstrapTokenStore::new(cache).await;
let peer_id = hex::encode([0xAA; 32]);
store.insert(&peer_id, Bytes::from_static(b"first_token"));
store.insert(&peer_id, Bytes::from_static(b"second_token"));
let taken = store.take(&peer_id);
assert_eq!(
taken,
Some(Bytes::from_static(b"second_token")),
"Should return the most recently inserted token"
);
}
}