http_client_vcr/
form_data.rs

1use std::collections::HashMap;
2
3/// Parse URL-encoded form data into key-value pairs
4pub fn parse_form_data(data: &str) -> HashMap<String, String> {
5    let mut params = HashMap::new();
6
7    for pair in data.split('&') {
8        if let Some((key, value)) = pair.split_once('=') {
9            // URL decode the key and value
10            let decoded_key = urlencoding::decode(key).unwrap_or_else(|_| key.into());
11            let decoded_value = urlencoding::decode(value).unwrap_or_else(|_| value.into());
12            params.insert(decoded_key.to_string(), decoded_value.to_string());
13        }
14    }
15
16    params
17}
18
19/// Encode form data back to URL-encoded string
20pub fn encode_form_data(params: &HashMap<String, String>) -> String {
21    params
22        .iter()
23        .map(|(key, value)| {
24            format!(
25                "{}={}",
26                urlencoding::encode(key),
27                urlencoding::encode(value)
28            )
29        })
30        .collect::<Vec<_>>()
31        .join("&")
32}
33
34/// Detect potential credential fields in form data
35pub fn find_credential_fields(params: &HashMap<String, String>) -> Vec<(String, String)> {
36    let mut credentials = Vec::new();
37
38    // Common field names that might contain credentials
39    let credential_patterns = [
40        // Username patterns
41        "username",
42        "user",
43        "login",
44        "email",
45        "username_or_email",
46        "user_name",
47        // Password patterns
48        "password",
49        "pass",
50        "passwd",
51        "pwd",
52        "secret",
53        // Token/CSRF patterns
54        "_token",
55        // Session patterns
56        "session",
57        "sessionid",
58        "sid",
59        "auth",
60        "authorization",
61        // API key patterns
62        "api_key",
63        "apikey",
64        "key",
65        "client_secret",
66        "access_token",
67        "refresh_token",
68    ];
69
70    for (key, value) in params {
71        let key_lower = key.to_lowercase();
72
73        // Check if the key matches any credential pattern
74        for pattern in &credential_patterns {
75            if key_lower.contains(pattern) {
76                credentials.push((key.clone(), value.clone()));
77                break;
78            }
79        }
80
81        // Also check for suspicious values (long alphanumeric strings that might be tokens)
82        if value.len() > 10 && value.chars().all(|c| c.is_alphanumeric()) {
83            // This might be a token or hash
84            credentials.push((key.clone(), value.clone()));
85        }
86    }
87
88    credentials
89}
90
91/// Filter sensitive form data by replacing credential values
92pub fn filter_form_data(data: &str, replacement_pattern: &str) -> String {
93    let mut params = parse_form_data(data);
94    let credentials = find_credential_fields(&params);
95
96    // Replace sensitive values
97    for (key, _) in credentials {
98        if let Some(value) = params.get_mut(&key) {
99            *value = format!("{replacement_pattern}_{}", key.to_uppercase());
100        }
101    }
102
103    encode_form_data(&params)
104}
105
106/// Analyze form data and return a report of what was found
107pub fn analyze_form_data(data: &str) -> FormDataAnalysis {
108    let params = parse_form_data(data);
109    let credentials = find_credential_fields(&params);
110
111    FormDataAnalysis {
112        total_fields: params.len(),
113        credential_fields: credentials,
114        all_fields: params,
115    }
116}
117
118#[derive(Debug)]
119pub struct FormDataAnalysis {
120    pub total_fields: usize,
121    pub credential_fields: Vec<(String, String)>,
122    pub all_fields: HashMap<String, String>,
123}
124
125impl FormDataAnalysis {
126    /// Print a summary of the analysis
127    pub fn print_summary(&self) {
128        println!("Form Data Analysis:");
129        println!("  Total fields: {}", self.total_fields);
130        println!(
131            "  Credential fields found: {}",
132            self.credential_fields.len()
133        );
134
135        if !self.credential_fields.is_empty() {
136            println!("  Sensitive fields:");
137            for (key, value) in &self.credential_fields {
138                let preview = if value.len() > 20 {
139                    format!("{}...", &value[..20])
140                } else {
141                    value.clone()
142                };
143                println!("    {key}: {preview}");
144            }
145        }
146
147        println!("  All fields:");
148        for (key, value) in &self.all_fields {
149            let preview = if value.len() > 50 {
150                format!("{}...", &value[..50])
151            } else {
152                value.clone()
153            };
154            println!("    {key}: {preview}");
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_parse_form_data() {
165        let data = "username=testuser&password=secret123&csrf_token=abc123";
166        let params = parse_form_data(data);
167
168        assert_eq!(params.get("username"), Some(&"testuser".to_string()));
169        assert_eq!(params.get("password"), Some(&"secret123".to_string()));
170        assert_eq!(params.get("csrf_token"), Some(&"abc123".to_string()));
171    }
172
173    #[test]
174    fn test_find_credential_fields() {
175        let mut params = HashMap::new();
176        params.insert("username".to_string(), "testuser".to_string());
177        params.insert("password".to_string(), "secret123".to_string());
178        params.insert("normal_field".to_string(), "value".to_string());
179
180        let credentials = find_credential_fields(&params);
181        assert_eq!(credentials.len(), 2);
182
183        let keys: Vec<&String> = credentials.iter().map(|(k, _)| k).collect();
184        assert!(keys.contains(&&"username".to_string()));
185        assert!(keys.contains(&&"password".to_string()));
186    }
187
188    #[test]
189    fn test_filter_form_data() {
190        let data = "username=testuser&password=secret123&normal=value";
191        let filtered = filter_form_data(data, "[FILTERED]");
192
193        // The brackets get URL-encoded, so we need to check for the encoded version
194        assert!(filtered.contains("%5BFILTERED%5D_USERNAME"));
195        assert!(filtered.contains("%5BFILTERED%5D_PASSWORD"));
196        assert!(filtered.contains("normal=value"));
197    }
198}