1use reqwest::Client;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::Duration;
8use tracing::{debug, warn};
9
10use super::protocol::MirrorPayload;
11
12pub struct ShadowMirrorClient {
16 http_client: Client,
18 hmac_secret: Option<String>,
20 successes: AtomicU64,
22 failures: AtomicU64,
24 bytes_sent: AtomicU64,
26}
27
28impl ShadowMirrorClient {
29 pub fn new(hmac_secret: Option<String>, timeout: Duration) -> Result<Self, ShadowMirrorError> {
38 let http_client = Client::builder()
39 .timeout(timeout)
40 .pool_max_idle_per_host(10)
41 .pool_idle_timeout(Duration::from_secs(30))
42 .connect_timeout(Duration::from_secs(5))
43 .build()
44 .map_err(|e| ShadowMirrorError::ClientCreation(e.to_string()))?;
45
46 Ok(Self {
47 http_client,
48 hmac_secret,
49 successes: AtomicU64::new(0),
50 failures: AtomicU64::new(0),
51 bytes_sent: AtomicU64::new(0),
52 })
53 }
54
55 pub async fn send_to_honeypot(
59 &self,
60 urls: &[String],
61 payload: MirrorPayload,
62 timeout: Duration,
63 ) -> Result<(), ShadowMirrorError> {
64 if urls.is_empty() {
65 return Err(ShadowMirrorError::NoHoneypotUrls);
66 }
67
68 let url_index = self.select_url_index(&payload.request_id, urls.len());
70 let url = &urls[url_index];
71
72 let json = payload
73 .to_json_bytes()
74 .map_err(ShadowMirrorError::Serialization)?;
75 let json_len = json.len() as u64;
76
77 let mut request = self
78 .http_client
79 .post(url)
80 .timeout(timeout)
81 .header("Content-Type", "application/json")
82 .header("X-Shadow-Mirror", "1")
83 .header("X-Request-ID", &payload.request_id)
84 .header("X-Protocol-Version", &payload.protocol_version);
85
86 if let Some(ref secret) = self.hmac_secret {
88 let signature = self.compute_hmac(secret, &json);
89 request = request.header("X-Signature", signature);
90 }
91
92 debug!(
93 url = %url,
94 request_id = %payload.request_id,
95 payload_size = json_len,
96 "Sending shadow mirror payload"
97 );
98
99 let result = request.body(json).send().await;
100
101 match result {
102 Ok(response) => {
103 if response.status().is_success() {
104 self.successes.fetch_add(1, Ordering::Relaxed);
105 self.bytes_sent.fetch_add(json_len, Ordering::Relaxed);
106 debug!(
107 url = %url,
108 request_id = %payload.request_id,
109 status = %response.status(),
110 "Shadow mirror delivery succeeded"
111 );
112 Ok(())
113 } else {
114 self.failures.fetch_add(1, Ordering::Relaxed);
115 warn!(
116 url = %url,
117 request_id = %payload.request_id,
118 status = %response.status(),
119 "Shadow mirror delivery failed with non-success status"
120 );
121 Err(ShadowMirrorError::HttpError {
122 status: response.status().as_u16(),
123 url: url.clone(),
124 })
125 }
126 }
127 Err(e) => {
128 self.failures.fetch_add(1, Ordering::Relaxed);
129 warn!(
130 url = %url,
131 request_id = %payload.request_id,
132 error = %e,
133 "Shadow mirror delivery failed"
134 );
135 Err(ShadowMirrorError::RequestFailed {
136 url: url.clone(),
137 reason: e.to_string(),
138 })
139 }
140 }
141 }
142
143 fn select_url_index(&self, request_id: &str, url_count: usize) -> usize {
145 let mut hash: u64 = 14695981039346656037; for byte in request_id.bytes() {
148 hash ^= byte as u64;
149 hash = hash.wrapping_mul(1099511628211); }
151
152 (hash as usize) % url_count
153 }
154
155 fn compute_hmac(&self, secret: &str, data: &[u8]) -> String {
157 use hmac::{Hmac, Mac};
158 use sha2::Sha256;
159
160 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
161 .expect("HMAC can accept any key length");
162 mac.update(data);
163 hex::encode(mac.finalize().into_bytes())
164 }
165
166 pub fn stats(&self) -> ShadowClientStats {
168 ShadowClientStats {
169 successes: self.successes.load(Ordering::Relaxed),
170 failures: self.failures.load(Ordering::Relaxed),
171 bytes_sent: self.bytes_sent.load(Ordering::Relaxed),
172 }
173 }
174
175 pub fn reset_stats(&self) {
177 self.successes.store(0, Ordering::Relaxed);
178 self.failures.store(0, Ordering::Relaxed);
179 self.bytes_sent.store(0, Ordering::Relaxed);
180 }
181}
182
183#[derive(Debug, Clone, serde::Serialize)]
185pub struct ShadowClientStats {
186 pub successes: u64,
188 pub failures: u64,
190 pub bytes_sent: u64,
192}
193
194impl ShadowClientStats {
195 pub fn success_rate(&self) -> f64 {
197 let total = self.successes + self.failures;
198 if total == 0 {
199 100.0
200 } else {
201 (self.successes as f64 / total as f64) * 100.0
202 }
203 }
204}
205
206#[derive(Debug, thiserror::Error)]
208pub enum ShadowMirrorError {
209 #[error("failed to create HTTP client: {0}")]
210 ClientCreation(String),
211
212 #[error("no honeypot URLs configured")]
213 NoHoneypotUrls,
214
215 #[error("failed to serialize payload: {0}")]
216 Serialization(#[from] serde_json::Error),
217
218 #[error("HTTP request to {url} failed with status {status}")]
219 HttpError { status: u16, url: String },
220
221 #[error("request to {url} failed: {reason}")]
222 RequestFailed { url: String, reason: String },
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 fn create_test_payload() -> MirrorPayload {
230 MirrorPayload::new(
231 "test-request-id".to_string(),
232 "192.168.1.100".to_string(),
233 55.0,
234 "POST".to_string(),
235 "/api/login".to_string(),
236 "example.com".to_string(),
237 "sensor-01".to_string(),
238 )
239 }
240
241 #[test]
242 fn test_client_creation() {
243 let client = ShadowMirrorClient::new(None, Duration::from_secs(5))
244 .expect("client creation should succeed");
245 let stats = client.stats();
246 assert_eq!(stats.successes, 0);
247 assert_eq!(stats.failures, 0);
248 }
249
250 #[test]
251 fn test_client_with_hmac() {
252 let client =
253 ShadowMirrorClient::new(Some("my-secret-key".to_string()), Duration::from_secs(5))
254 .expect("client creation should succeed");
255 assert!(client.hmac_secret.is_some());
256 }
257
258 #[test]
259 fn test_hmac_computation() {
260 let client =
261 ShadowMirrorClient::new(Some("test-secret".to_string()), Duration::from_secs(5))
262 .expect("client creation should succeed");
263
264 let data = b"test payload data";
265 let signature = client.compute_hmac("test-secret", data);
266
267 assert_eq!(signature.len(), 64);
269 let signature2 = client.compute_hmac("test-secret", data);
271 assert_eq!(signature, signature2);
272 }
273
274 #[test]
275 fn test_url_selection_distribution() {
276 let client = ShadowMirrorClient::new(None, Duration::from_secs(5))
277 .expect("client creation should succeed");
278 let urls = 3;
279
280 let mut counts = [0u32; 3];
281
282 for i in 0..100 {
284 let request_id = format!("request-{}", i);
285 let index = client.select_url_index(&request_id, urls);
286 counts[index] += 1;
287 }
288
289 for (i, count) in counts.iter().enumerate() {
291 assert!(*count > 0, "URL {} got no traffic", i);
292 }
293 }
294
295 #[test]
296 fn test_url_selection_consistent() {
297 let client = ShadowMirrorClient::new(None, Duration::from_secs(5))
298 .expect("client creation should succeed");
299
300 let request_id = "consistent-request-id";
302 let first = client.select_url_index(request_id, 5);
303 let second = client.select_url_index(request_id, 5);
304 assert_eq!(first, second);
305 }
306
307 #[test]
308 fn test_stats_reset() {
309 let client = ShadowMirrorClient::new(None, Duration::from_secs(5))
310 .expect("client creation should succeed");
311
312 client.successes.store(10, Ordering::Relaxed);
314 client.failures.store(5, Ordering::Relaxed);
315
316 let stats = client.stats();
317 assert_eq!(stats.successes, 10);
318 assert_eq!(stats.failures, 5);
319
320 client.reset_stats();
321
322 let stats = client.stats();
323 assert_eq!(stats.successes, 0);
324 assert_eq!(stats.failures, 0);
325 }
326
327 #[test]
328 fn test_success_rate() {
329 let stats = ShadowClientStats {
330 successes: 90,
331 failures: 10,
332 bytes_sent: 1000,
333 };
334 assert!((stats.success_rate() - 90.0).abs() < 0.01);
335
336 let stats = ShadowClientStats {
337 successes: 0,
338 failures: 0,
339 bytes_sent: 0,
340 };
341 assert!((stats.success_rate() - 100.0).abs() < 0.01);
342 }
343
344 #[tokio::test]
345 async fn test_send_empty_urls() {
346 let client = ShadowMirrorClient::new(None, Duration::from_secs(1))
347 .expect("client creation should succeed");
348 let payload = create_test_payload();
349
350 let result = client
351 .send_to_honeypot(&[], payload, Duration::from_secs(1))
352 .await;
353 assert!(matches!(result, Err(ShadowMirrorError::NoHoneypotUrls)));
354 }
355}