Skip to main content

ant_quic/bootstrap_cache/
token_store.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Token persistence integration with BootstrapCache.
9
10use crate::bootstrap_cache::BootstrapCache;
11use crate::nat_traversal_api::PeerId;
12use crate::token::TokenStore;
13use bytes::Bytes;
14use std::collections::HashMap;
15use std::sync::{Arc, RwLock};
16use tracing::{debug, warn};
17
18/// A TokenStore implementation that persists tokens to the BootstrapCache.
19///
20/// It maintains a local synchronous cache for `take` operations (required by `TokenStore` trait)
21/// and asynchronously updates the `BootstrapCache` on `insert`.
22#[derive(Debug)]
23pub struct BootstrapTokenStore {
24    /// Reference to the persistent cache
25    cache: Arc<BootstrapCache>,
26    /// Local synchronous cache: ServerName -> Token
27    /// ServerName is expected to be a PeerId hex string or a specific IP Key.
28    local_cache: Arc<RwLock<HashMap<String, Vec<u8>>>>,
29}
30
31impl BootstrapTokenStore {
32    /// Create a new BootstrapTokenStore backed by the given cache.
33    ///
34    /// This will initialize the local memory cache with all tokens currently in the BootstrapCache.
35    pub async fn new(cache: Arc<BootstrapCache>) -> Self {
36        let tokens = cache.get_all_tokens().await;
37        let mut local = HashMap::new();
38
39        for (peer_id, token) in tokens {
40            // Key by PeerId hex string
41            let key = hex::encode(peer_id.0);
42            local.insert(key, token);
43        }
44
45        debug!(
46            "Initialized BootstrapTokenStore with {} tokens",
47            local.len()
48        );
49
50        Self {
51            cache,
52            local_cache: Arc::new(RwLock::new(local)),
53        }
54    }
55}
56
57impl TokenStore for BootstrapTokenStore {
58    fn insert(&self, server_name: &str, token: Bytes) {
59        let token_vec = token.to_vec();
60
61        // 1. Update local cache immediately
62        if let Ok(mut local) = self.local_cache.write() {
63            local.insert(server_name.to_string(), token_vec.clone());
64        } else {
65            warn!("Failed to acquire write lock on local token cache");
66        }
67
68        // 2. Try to parse server_name as PeerId and update persistent cache
69        // server_name is expected to be hex-encoded PeerId
70        if let Ok(bytes) = hex::decode(server_name) {
71            if let Ok(arr) = <[u8; 32]>::try_from(bytes) {
72                let peer_id = PeerId(arr);
73                let cache = self.cache.clone();
74                let token_clone = token_vec;
75
76                // Spawn async task to update persistent cache
77                tokio::spawn(async move {
78                    cache.update_token(peer_id, token_clone).await;
79                });
80                return;
81            }
82        }
83
84        // If server_name is not a PeerId (e.g. it's an IP), we can't persist it
85        // to a specific Peer entry easily unless we do a reverse lookup.
86        // For now, we only persist tokens if the SNI was the PeerId.
87        debug!(
88            "Received token for non-PeerId server name: {}, not persisting to disk",
89            server_name
90        );
91    }
92
93    fn take(&self, server_name: &str) -> Option<Bytes> {
94        if let Ok(mut local) = self.local_cache.write() {
95            local.remove(server_name).map(Bytes::from)
96        } else {
97            warn!("Failed to acquire write lock on local token cache");
98            None
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::bootstrap_cache::BootstrapCacheConfig;
107    use tempfile::TempDir;
108
109    async fn create_test_cache(temp_dir: &TempDir) -> Arc<BootstrapCache> {
110        let config = BootstrapCacheConfig::builder()
111            .cache_dir(temp_dir.path())
112            .max_peers(100)
113            .epsilon(0.0)
114            .min_peers_to_save(1)
115            .build();
116
117        Arc::new(
118            BootstrapCache::open(config)
119                .await
120                .expect("Failed to create cache"),
121        )
122    }
123
124    #[tokio::test]
125    async fn insert_and_take_valid_peer_id() {
126        let temp_dir = TempDir::new().expect("Failed to create temp dir");
127        let cache = create_test_cache(&temp_dir).await;
128        let store = BootstrapTokenStore::new(cache).await;
129
130        // Valid PeerId hex string (32 bytes = 64 hex chars)
131        let peer_id_hex = hex::encode([0xAB; 32]);
132        let token = Bytes::from_static(b"test_token_data");
133
134        // Insert token
135        store.insert(&peer_id_hex, token.clone());
136
137        // First take should return the token
138        let taken = store.take(&peer_id_hex);
139        assert!(taken.is_some(), "First take should return token");
140        assert_eq!(taken.expect("should have token"), token);
141
142        // Second take should return None (one-shot semantics)
143        let taken_again = store.take(&peer_id_hex);
144        assert!(
145            taken_again.is_none(),
146            "Second take should return None (one-shot)"
147        );
148    }
149
150    #[tokio::test]
151    async fn take_nonexistent_returns_none() {
152        let temp_dir = TempDir::new().expect("Failed to create temp dir");
153        let cache = create_test_cache(&temp_dir).await;
154        let store = BootstrapTokenStore::new(cache).await;
155
156        let result = store.take("nonexistent_key");
157        assert!(result.is_none());
158    }
159
160    #[tokio::test]
161    async fn insert_non_peer_id_server_name() {
162        let temp_dir = TempDir::new().expect("Failed to create temp dir");
163        let cache = create_test_cache(&temp_dir).await;
164        let store = BootstrapTokenStore::new(cache).await;
165
166        // Non-PeerId server names (IPs, hostnames)
167        let test_cases = ["192.168.1.1:8000", "server.example.com", "localhost", "::1"];
168
169        for server_name in test_cases {
170            let token = Bytes::from(format!("token_for_{}", server_name));
171
172            // Insert should succeed locally even for non-PeerId names
173            store.insert(server_name, token.clone());
174
175            // Take should work (local cache)
176            let taken = store.take(server_name);
177            assert!(
178                taken.is_some(),
179                "Should be able to take token for {}",
180                server_name
181            );
182            assert_eq!(taken.expect("should have token"), token);
183        }
184    }
185
186    #[tokio::test]
187    async fn hex_decode_edge_cases() {
188        let temp_dir = TempDir::new().expect("Failed to create temp dir");
189        let cache = create_test_cache(&temp_dir).await;
190        let store = BootstrapTokenStore::new(cache).await;
191
192        // Test various malformed hex strings - should still work via local cache
193        let edge_cases = [
194            "",                       // Empty string
195            "abc",                    // Odd length (not valid hex length)
196            "gggg",                   // Invalid hex chars
197            "00112233",               // Valid hex but wrong length (4 bytes, not 32)
198            &hex::encode([0xFF; 16]), // 16 bytes instead of 32
199        ];
200
201        for server_name in edge_cases {
202            let token = Bytes::from_static(b"edge_case_token");
203
204            // Insert should succeed (updates local cache)
205            store.insert(server_name, token.clone());
206
207            // Take should work from local cache
208            let taken = store.take(server_name);
209            assert!(
210                taken.is_some(),
211                "Should take token for edge case: '{}'",
212                server_name
213            );
214        }
215    }
216
217    #[tokio::test]
218    async fn multiple_tokens_different_peers() {
219        let temp_dir = TempDir::new().expect("Failed to create temp dir");
220        let cache = create_test_cache(&temp_dir).await;
221        let store = BootstrapTokenStore::new(cache).await;
222
223        // Insert tokens for multiple peers
224        let peer1 = hex::encode([0x11; 32]);
225        let peer2 = hex::encode([0x22; 32]);
226        let peer3 = hex::encode([0x33; 32]);
227
228        store.insert(&peer1, Bytes::from_static(b"token1"));
229        store.insert(&peer2, Bytes::from_static(b"token2"));
230        store.insert(&peer3, Bytes::from_static(b"token3"));
231
232        // Each peer should have their own token
233        assert_eq!(store.take(&peer1), Some(Bytes::from_static(b"token1")));
234        assert_eq!(store.take(&peer2), Some(Bytes::from_static(b"token2")));
235        assert_eq!(store.take(&peer3), Some(Bytes::from_static(b"token3")));
236
237        // All should be gone now
238        assert!(store.take(&peer1).is_none());
239        assert!(store.take(&peer2).is_none());
240        assert!(store.take(&peer3).is_none());
241    }
242
243    #[tokio::test]
244    async fn overwrite_token_for_same_peer() {
245        let temp_dir = TempDir::new().expect("Failed to create temp dir");
246        let cache = create_test_cache(&temp_dir).await;
247        let store = BootstrapTokenStore::new(cache).await;
248
249        let peer_id = hex::encode([0xAA; 32]);
250
251        // Insert first token
252        store.insert(&peer_id, Bytes::from_static(b"first_token"));
253
254        // Overwrite with second token
255        store.insert(&peer_id, Bytes::from_static(b"second_token"));
256
257        // Should get the second (newest) token
258        let taken = store.take(&peer_id);
259        assert_eq!(
260            taken,
261            Some(Bytes::from_static(b"second_token")),
262            "Should return the most recently inserted token"
263        );
264    }
265}