Skip to main content

moonlight_core/compare/
capture.rs

1use 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}