rust_x402/
facilitator_storage.rs1use crate::Result;
7use async_trait::async_trait;
8
9#[async_trait]
14pub trait NonceStorage: Send + Sync {
15 async fn has_nonce(&self, nonce: &str) -> Result<bool>;
17
18 async fn mark_nonce(&self, nonce: &str) -> Result<()>;
20
21 async fn remove_nonce(&self, nonce: &str) -> Result<()>;
23}
24
25#[derive(Debug, Clone)]
30pub struct InMemoryStorage {
31 nonces: std::sync::Arc<tokio::sync::RwLock<std::collections::HashMap<String, bool>>>,
32}
33
34impl InMemoryStorage {
35 pub fn new() -> Self {
37 Self {
38 nonces: std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
39 }
40 }
41}
42
43impl Default for InMemoryStorage {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49#[async_trait]
50impl NonceStorage for InMemoryStorage {
51 async fn has_nonce(&self, nonce: &str) -> Result<bool> {
52 let nonces = self.nonces.read().await;
53 Ok(nonces.contains_key(nonce))
54 }
55
56 async fn mark_nonce(&self, nonce: &str) -> Result<()> {
57 let mut nonces = self.nonces.write().await;
58 nonces.insert(nonce.to_string(), true);
59 Ok(())
60 }
61
62 async fn remove_nonce(&self, nonce: &str) -> Result<()> {
63 let mut nonces = self.nonces.write().await;
64 nonces.remove(nonce);
65 Ok(())
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[tokio::test]
74 async fn test_in_memory_storage_creation() {
75 let storage = InMemoryStorage::new();
76 assert!(storage.has_nonce("test").await.unwrap() == false);
77 }
78
79 #[tokio::test]
80 async fn test_in_memory_storage_has_nonce() {
81 let storage = InMemoryStorage::new();
82 let test_nonce = "test_nonce_123";
83
84 let exists = storage.has_nonce(test_nonce).await.unwrap();
86 assert!(!exists, "Nonce should not exist initially");
87
88 storage.mark_nonce(test_nonce).await.unwrap();
90
91 let exists = storage.has_nonce(test_nonce).await.unwrap();
93 assert!(exists, "Nonce should exist after marking");
94 }
95
96 #[tokio::test]
97 async fn test_in_memory_storage_mark_nonce() {
98 let storage = InMemoryStorage::new();
99 let test_nonce = "test_nonce_mark_456";
100
101 let result = storage.mark_nonce(test_nonce).await;
103 assert!(result.is_ok(), "mark_nonce should succeed");
104
105 let exists = storage.has_nonce(test_nonce).await.unwrap();
107 assert!(exists, "Nonce should exist after marking");
108 }
109
110 #[tokio::test]
111 async fn test_in_memory_storage_remove_nonce() {
112 let storage = InMemoryStorage::new();
113 let test_nonce = "test_nonce_remove_789";
114
115 storage.mark_nonce(test_nonce).await.unwrap();
117 assert!(storage.has_nonce(test_nonce).await.unwrap());
118
119 let result = storage.remove_nonce(test_nonce).await;
121 assert!(result.is_ok(), "remove_nonce should succeed");
122
123 let exists = storage.has_nonce(test_nonce).await.unwrap();
125 assert!(!exists, "Nonce should not exist after removal");
126 }
127
128 #[tokio::test]
129 async fn test_in_memory_storage_replay_protection() {
130 let storage = InMemoryStorage::new();
131 let test_nonce = "test_nonce_replay_abc";
132
133 assert!(storage.has_nonce(test_nonce).await.unwrap() == false);
135 storage.mark_nonce(test_nonce).await.unwrap();
136
137 storage.mark_nonce(test_nonce).await.unwrap();
139 assert!(
140 storage.has_nonce(test_nonce).await.unwrap(),
141 "Nonce should still exist after second mark"
142 );
143 }
144
145 #[tokio::test]
146 async fn test_in_memory_storage_multiple_nonces() {
147 let storage = InMemoryStorage::new();
148
149 let nonce1 = "nonce1";
150 let nonce2 = "nonce2";
151 let nonce3 = "nonce3";
152
153 storage.mark_nonce(nonce1).await.unwrap();
155 storage.mark_nonce(nonce2).await.unwrap();
156 storage.mark_nonce(nonce3).await.unwrap();
157
158 assert!(storage.has_nonce(nonce1).await.unwrap());
160 assert!(storage.has_nonce(nonce2).await.unwrap());
161 assert!(storage.has_nonce(nonce3).await.unwrap());
162
163 storage.remove_nonce(nonce2).await.unwrap();
165 assert!(!storage.has_nonce(nonce2).await.unwrap());
166 assert!(storage.has_nonce(nonce1).await.unwrap());
167 assert!(storage.has_nonce(nonce3).await.unwrap());
168 }
169}
170
171#[cfg(feature = "redis")]
172pub mod redis_storage {
173 use super::{NonceStorage, Result};
174 use redis::{AsyncCommands, Client};
175
176 #[derive(Debug, Clone)]
182 pub struct RedisStorage {
183 client: Client,
184 key_prefix: String,
185 }
186
187 impl RedisStorage {
188 pub async fn new(redis_url: &str, key_prefix: Option<&str>) -> Result<Self> {
195 let client = Client::open(redis_url).map_err(|e| {
196 crate::X402Error::config(format!("Failed to connect to Redis: {}", e))
197 })?;
198
199 let key_prefix = key_prefix.unwrap_or("x402:nonce:").to_string();
200
201 Ok(Self { client, key_prefix })
202 }
203
204 fn make_key(&self, nonce: &str) -> String {
205 format!("{}{}", self.key_prefix, nonce)
206 }
207 }
208
209 #[async_trait::async_trait]
210 impl NonceStorage for RedisStorage {
211 async fn has_nonce(&self, nonce: &str) -> Result<bool> {
212 let mut conn = self
213 .client
214 .get_multiplexed_async_connection()
215 .await
216 .map_err(|e| {
217 crate::X402Error::config(format!("Failed to get Redis connection: {}", e))
218 })?;
219
220 let key = self.make_key(nonce);
221 let exists: bool = conn.exists(&key).await.map_err(|e| {
222 crate::X402Error::config(format!("Redis EXISTS command failed: {}", e))
223 })?;
224
225 Ok(exists)
226 }
227
228 async fn mark_nonce(&self, nonce: &str) -> Result<()> {
229 let mut conn = self
230 .client
231 .get_multiplexed_async_connection()
232 .await
233 .map_err(|e| {
234 crate::X402Error::config(format!("Failed to get Redis connection: {}", e))
235 })?;
236
237 let key = self.make_key(nonce);
238 conn.set_ex::<_, _, ()>(&key, "1", 86400)
240 .await
241 .map_err(|e| {
242 crate::X402Error::config(format!("Redis SET command failed: {}", e))
243 })?;
244
245 Ok(())
246 }
247
248 async fn remove_nonce(&self, nonce: &str) -> Result<()> {
249 let mut conn = self
250 .client
251 .get_multiplexed_async_connection()
252 .await
253 .map_err(|e| {
254 crate::X402Error::config(format!("Failed to get Redis connection: {}", e))
255 })?;
256
257 let key = self.make_key(nonce);
258 conn.del::<_, ()>(&key).await.map_err(|e| {
259 crate::X402Error::config(format!("Redis DEL command failed: {}", e))
260 })?;
261
262 Ok(())
263 }
264 }
265
266 #[cfg(test)]
267 mod tests {
268 use super::*;
269 use std::env;
270
271 async fn check_redis_available(redis_url: &str) -> bool {
274 match Client::open(redis_url) {
275 Ok(client) => {
276 match client.get_multiplexed_async_connection().await {
277 Ok(mut conn) => {
278 match conn.get::<&str, Option<String>>("__test_key__").await {
280 Ok(_) => true,
281 Err(_) => {
282 match conn.exists::<&str, bool>("__test_key__").await {
284 Ok(_) => true,
285 Err(_) => false,
286 }
287 }
288 }
289 }
290 Err(_) => false,
291 }
292 }
293 Err(_) => false,
294 }
295 }
296
297 #[tokio::test]
298 async fn test_redis_storage_creation() {
299 let redis_url =
300 env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
301
302 if !check_redis_available(&redis_url).await {
303 println!("Skipping Redis test: Redis not available at {}", redis_url);
304 return;
305 }
306
307 let storage = RedisStorage::new(&redis_url, None).await;
308 assert!(storage.is_ok(), "RedisStorage creation should succeed");
309
310 let storage = storage.unwrap();
311 assert_eq!(storage.key_prefix, "x402:nonce:");
312 }
313
314 #[tokio::test]
315 async fn test_redis_storage_custom_prefix() {
316 let redis_url =
317 env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
318
319 if !check_redis_available(&redis_url).await {
320 println!("Skipping Redis test: Redis not available at {}", redis_url);
321 return;
322 }
323
324 let storage = RedisStorage::new(&redis_url, Some("test:prefix:")).await;
325 assert!(storage.is_ok());
326
327 let storage = storage.unwrap();
328 assert_eq!(storage.key_prefix, "test:prefix:");
329 }
330
331 #[tokio::test]
332 async fn test_redis_storage_has_nonce() {
333 let redis_url =
334 env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
335
336 if !check_redis_available(&redis_url).await {
337 println!("Skipping Redis test: Redis not available at {}", redis_url);
338 return;
339 }
340
341 let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
343 let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
344 .await
345 .unwrap();
346
347 let test_nonce = "test_nonce_123";
348
349 let exists = storage.has_nonce(test_nonce).await.unwrap();
351 assert!(!exists, "Nonce should not exist initially");
352
353 storage.mark_nonce(test_nonce).await.unwrap();
355
356 let exists = storage.has_nonce(test_nonce).await.unwrap();
358 assert!(exists, "Nonce should exist after marking");
359
360 storage.remove_nonce(test_nonce).await.unwrap();
362 }
363
364 #[tokio::test]
365 async fn test_redis_storage_mark_nonce() {
366 let redis_url =
367 env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
368
369 if !check_redis_available(&redis_url).await {
370 println!("Skipping Redis test: Redis not available at {}", redis_url);
371 return;
372 }
373
374 let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
375 let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
376 .await
377 .unwrap();
378
379 let test_nonce = "test_nonce_mark_456";
380
381 let result = storage.mark_nonce(test_nonce).await;
383 assert!(result.is_ok(), "mark_nonce should succeed");
384
385 let exists = storage.has_nonce(test_nonce).await.unwrap();
387 assert!(exists, "Nonce should exist after marking");
388
389 storage.remove_nonce(test_nonce).await.unwrap();
391 }
392
393 #[tokio::test]
394 async fn test_redis_storage_remove_nonce() {
395 let redis_url =
396 env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
397
398 if !check_redis_available(&redis_url).await {
399 println!("Skipping Redis test: Redis not available at {}", redis_url);
400 return;
401 }
402
403 let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
404 let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
405 .await
406 .unwrap();
407
408 let test_nonce = "test_nonce_remove_789";
409
410 storage.mark_nonce(test_nonce).await.unwrap();
412 assert!(storage.has_nonce(test_nonce).await.unwrap());
413
414 let result = storage.remove_nonce(test_nonce).await;
416 assert!(result.is_ok(), "remove_nonce should succeed");
417
418 let exists = storage.has_nonce(test_nonce).await.unwrap();
420 assert!(!exists, "Nonce should not exist after removal");
421 }
422
423 #[tokio::test]
424 async fn test_redis_storage_replay_protection() {
425 let redis_url =
426 env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
427
428 if !check_redis_available(&redis_url).await {
429 println!("Skipping Redis test: Redis not available at {}", redis_url);
430 return;
431 }
432
433 let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
434 let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
435 .await
436 .unwrap();
437
438 let test_nonce = "test_nonce_replay_abc";
439
440 assert!(storage.has_nonce(test_nonce).await.unwrap() == false);
442 storage.mark_nonce(test_nonce).await.unwrap();
443
444 storage.mark_nonce(test_nonce).await.unwrap();
446 assert!(
447 storage.has_nonce(test_nonce).await.unwrap(),
448 "Nonce should still exist after second mark"
449 );
450
451 storage.remove_nonce(test_nonce).await.unwrap();
453 }
454
455 #[tokio::test]
456 async fn test_redis_storage_ttl() {
457 let redis_url =
458 env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
459
460 if !check_redis_available(&redis_url).await {
461 println!("Skipping Redis test: Redis not available at {}", redis_url);
462 return;
463 }
464
465 let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
466 let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
467 .await
468 .unwrap();
469
470 let test_nonce = "test_nonce_ttl_xyz";
471
472 storage.mark_nonce(test_nonce).await.unwrap();
474
475 let mut conn = storage
477 .client
478 .get_multiplexed_async_connection()
479 .await
480 .unwrap();
481 let key = storage.make_key(test_nonce);
482 let ttl: i64 = conn.ttl(&key).await.unwrap();
483
484 assert!(ttl > 0, "Key should have a positive TTL");
486 assert!(
487 ttl <= 86400,
488 "TTL should be at most 24 hours (86400 seconds)"
489 );
490
491 storage.remove_nonce(test_nonce).await.unwrap();
493 }
494
495 #[tokio::test]
496 async fn test_redis_storage_multiple_nonces() {
497 let redis_url =
498 env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
499
500 if !check_redis_available(&redis_url).await {
501 println!("Skipping Redis test: Redis not available at {}", redis_url);
502 return;
503 }
504
505 let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
506 let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
507 .await
508 .unwrap();
509
510 let nonce1 = "nonce1";
511 let nonce2 = "nonce2";
512 let nonce3 = "nonce3";
513
514 storage.mark_nonce(nonce1).await.unwrap();
516 storage.mark_nonce(nonce2).await.unwrap();
517 storage.mark_nonce(nonce3).await.unwrap();
518
519 assert!(storage.has_nonce(nonce1).await.unwrap());
521 assert!(storage.has_nonce(nonce2).await.unwrap());
522 assert!(storage.has_nonce(nonce3).await.unwrap());
523
524 storage.remove_nonce(nonce2).await.unwrap();
526 assert!(!storage.has_nonce(nonce2).await.unwrap());
527 assert!(storage.has_nonce(nonce1).await.unwrap());
528 assert!(storage.has_nonce(nonce3).await.unwrap());
529
530 storage.remove_nonce(nonce1).await.unwrap();
532 storage.remove_nonce(nonce3).await.unwrap();
533 }
534 }
535}