moonlight_core/compare/
capture.rs1use crate::BodyCapture;
2use http::HeaderMap;
3use serde_json::Value;
4use sha2::{Digest, Sha256};
5use std::collections::{BTreeMap, HashSet};
6
7use super::json_path;
8
9pub fn capture_body(body: &[u8], max_bytes: usize) -> BodyCapture {
10 capture_body_with_redactions(body, max_bytes, &[])
11}
12
13pub fn capture_body_with_redactions(
14 body: &[u8],
15 max_bytes: usize,
16 redact_json_paths: &[String],
17) -> BodyCapture {
18 capture_body_with_redaction_patterns(body, max_bytes, redact_json_paths, &[])
19}
20
21pub fn capture_body_with_redaction_patterns(
22 body: &[u8],
23 max_bytes: usize,
24 redact_json_paths: &[String],
25 redact_json_path_patterns: &[String],
26) -> BodyCapture {
27 let mut hasher = Sha256::new();
28 hasher.update(body);
29 let preview_source = redact_json_body(body, redact_json_paths, redact_json_path_patterns)
30 .unwrap_or_else(|| body.to_vec());
31 let preview_bytes = &preview_source[..preview_source.len().min(max_bytes)];
32 BodyCapture {
33 size_bytes: body.len(),
34 sha256: hex::encode(hasher.finalize()),
35 preview: String::from_utf8_lossy(preview_bytes).to_string(),
36 truncated: preview_source.len() > max_bytes,
37 }
38}
39
40pub fn capture_headers(headers: &HeaderMap, redact_headers: &[String]) -> BTreeMap<String, String> {
41 let redact: HashSet<String> = redact_headers
42 .iter()
43 .map(|value| value.to_ascii_lowercase())
44 .collect();
45 let mut captured = BTreeMap::new();
46 for (name, value) in headers {
47 let key = name.as_str().to_ascii_lowercase();
48 if is_hop_by_hop_header(&key) {
49 continue;
50 }
51 let header_value = if redact.contains(&key) {
52 "[redacted]".to_string()
53 } else {
54 value.to_str().unwrap_or("[non-utf8]").to_string()
55 };
56 captured.insert(key, header_value);
57 }
58 captured
59}
60
61pub fn is_hop_by_hop_header(name: &str) -> bool {
62 matches!(
63 name.to_ascii_lowercase().as_str(),
64 "connection"
65 | "keep-alive"
66 | "proxy-authenticate"
67 | "proxy-authorization"
68 | "te"
69 | "trailer"
70 | "transfer-encoding"
71 | "upgrade"
72 | "host"
73 | "content-length"
74 )
75}
76
77fn redact_json_body(
78 body: &[u8],
79 redact_json_paths: &[String],
80 redact_json_path_patterns: &[String],
81) -> Option<Vec<u8>> {
82 if redact_json_paths.is_empty() && redact_json_path_patterns.is_empty() {
83 return None;
84 }
85
86 let mut value = serde_json::from_slice::<Value>(body).ok()?;
87 let mut changed = false;
88 for path in redact_json_paths {
89 if json_path::redact_value_at_path(&mut value, path) {
90 changed = true;
91 }
92 }
93 for pattern in redact_json_path_patterns {
94 if json_path::redact_value_at_matching_paths(&mut value, pattern) {
95 changed = true;
96 }
97 }
98
99 changed.then(|| serde_json::to_vec(&value).unwrap_or_else(|_| body.to_vec()))
100}