1use crate::error::BuildError;
7use crate::security::{OutputSanitizer, RateLimiter, SecurityConfig};
8use indexmap::{IndexMap, IndexSet};
9use std::time::{Duration, Instant};
10
11#[derive(Debug)]
13pub struct ApiSecurityManager {
14    rate_limiter: RateLimiter,
15    output_sanitizer: OutputSanitizer,
16    batch_monitor: BatchOperationMonitor,
17    ffi_validator: FfiValidator,
18    config: SecurityConfig,
19}
20
21impl ApiSecurityManager {
22    pub fn new(config: SecurityConfig) -> Self {
24        Self {
25            rate_limiter: RateLimiter::new(config.clone()),
26            output_sanitizer: OutputSanitizer::new(config.clone()),
27            batch_monitor: BatchOperationMonitor::new(config.clone()),
28            ffi_validator: FfiValidator::new(config.clone()),
29            config,
30        }
31    }
32
33    pub fn validate_request(
35        &mut self,
36        operation: &str,
37        identifier: &str,
38        payload_size: usize,
39    ) -> Result<(), BuildError> {
40        self.rate_limiter.check_rate_limit(identifier)?;
42
43        if payload_size > self.config.max_xml_size {
45            return Err(BuildError::Security(format!(
46                "Payload too large: {} bytes",
47                payload_size
48            )));
49        }
50
51        self.batch_monitor.track_operation(identifier, operation)?;
53
54        Ok(())
55    }
56
57    pub fn sanitize_response(&self, response: &str) -> Result<String, BuildError> {
59        self.output_sanitizer.sanitize_xml_output(response)
60    }
61
62    pub fn validate_ffi_input(
64        &self,
65        data: &[u8],
66        expected_type: FfiDataType,
67    ) -> Result<(), BuildError> {
68        self.ffi_validator.validate_input(data, expected_type)
69    }
70
71    pub fn get_wasm_security_headers(&self) -> IndexMap<String, String> {
73        let mut headers = IndexMap::new();
74
75        headers.insert(
77            "Content-Security-Policy".to_string(),
78            "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'".to_string(),
79        );
80
81        headers.insert("X-Content-Type-Options".to_string(), "nosniff".to_string());
83
84        headers.insert("X-Frame-Options".to_string(), "DENY".to_string());
86
87        headers.insert("X-XSS-Protection".to_string(), "1; mode=block".to_string());
89
90        headers.insert(
92            "Referrer-Policy".to_string(),
93            "strict-origin-when-cross-origin".to_string(),
94        );
95
96        headers.insert(
98            "Permissions-Policy".to_string(),
99            "camera=(), microphone=(), location=(), interest-cohort=()".to_string(),
100        );
101
102        headers
103    }
104
105    pub fn create_secure_error_response(&self, error: &BuildError, request_id: &str) -> String {
107        let sanitized_message = match error {
108            BuildError::Security(_) => "Security validation failed",
109            BuildError::InvalidFormat { .. } => "Invalid input format",
110            BuildError::Validation(..) => "Validation error",
111            BuildError::Io(_) => "I/O operation failed",
112            _ => "Internal error occurred",
113        };
114
115        let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
116
117        format!(
118            r#"{{"error": "{}", "request_id": "{}", "timestamp": "{}"}}"#,
119            sanitized_message, request_id, timestamp
120        )
121    }
122}
123
124#[derive(Debug)]
126pub struct BatchOperationMonitor {
127    operations: IndexMap<String, Vec<OperationRecord>>,
128    config: SecurityConfig,
129}
130
131#[derive(Debug, Clone)]
132#[allow(dead_code)]
133struct OperationRecord {
134    operation: String,
135    timestamp: Instant,
136    resource_usage: usize,
137}
138
139impl BatchOperationMonitor {
140    pub fn new(config: SecurityConfig) -> Self {
142        Self {
143            operations: IndexMap::new(),
144            config,
145        }
146    }
147
148    pub fn track_operation(&mut self, identifier: &str, operation: &str) -> Result<(), BuildError> {
150        let now = Instant::now();
151        let records = self.operations.entry(identifier.to_string()).or_default();
152
153        records.retain(|record| now.duration_since(record.timestamp) <= Duration::from_secs(60));
155
156        if records.len() >= self.config.max_requests_per_minute as usize {
158            return Err(BuildError::Security(
159                "Batch operation limit exceeded".to_string(),
160            ));
161        }
162
163        records.push(OperationRecord {
165            operation: operation.to_string(),
166            timestamp: now,
167            resource_usage: 1, });
169
170        Ok(())
171    }
172
173    pub fn get_stats(&self, identifier: &str) -> Option<BatchStats> {
175        let records = self.operations.get(identifier)?;
176        let now = Instant::now();
177
178        let recent_records: Vec<_> = records
179            .iter()
180            .filter(|r| now.duration_since(r.timestamp) <= Duration::from_secs(60))
181            .collect();
182
183        Some(BatchStats {
184            total_operations: recent_records.len(),
185            unique_operations: recent_records
186                .iter()
187                .map(|r| r.operation.as_str())
188                .collect::<IndexSet<_>>()
189                .len(),
190            time_window_seconds: 60,
191        })
192    }
193}
194
195#[derive(Debug)]
197pub struct RateLimitInfo {
198    pub total_operations: usize,
200    pub unique_operations: usize,
202    pub time_window_seconds: u64,
204}
205
206#[derive(Debug, Clone, Copy)]
208pub enum ContentType {
209    Xml,
211    Json,
213    Binary,
215    Utf8String,
217}
218
219#[derive(Debug, Clone)]
221pub struct BatchStats {
222    pub total_operations: usize,
224    pub unique_operations: usize,
226    pub time_window_seconds: u64,
228}
229
230#[derive(Debug)]
232pub struct FfiValidator {
233    config: SecurityConfig,
234}
235
236#[derive(Debug, Clone, Copy)]
238pub enum FfiDataType {
239    Xml,
241    Json,
243    Binary,
245    Utf8String,
247}
248
249impl FfiValidator {
250    pub fn new(config: SecurityConfig) -> Self {
252        Self { config }
253    }
254
255    pub fn validate_input(
257        &self,
258        data: &[u8],
259        expected_type: FfiDataType,
260    ) -> Result<(), BuildError> {
261        if data.len() > self.config.max_xml_size {
263            return Err(BuildError::Security(format!(
264                "FFI input too large: {} bytes",
265                data.len()
266            )));
267        }
268
269        match expected_type {
271            FfiDataType::Utf8String => {
272                std::str::from_utf8(data)
273                    .map_err(|_| BuildError::Security("Invalid UTF-8 in FFI input".to_string()))?;
274            }
275            FfiDataType::Xml => {
276                let xml_str = std::str::from_utf8(data).map_err(|_| {
277                    BuildError::Security("Invalid UTF-8 in XML FFI input".to_string())
278                })?;
279                self.validate_xml_structure(xml_str)?;
280            }
281            FfiDataType::Json => {
282                let json_str = std::str::from_utf8(data).map_err(|_| {
283                    BuildError::Security("Invalid UTF-8 in JSON FFI input".to_string())
284                })?;
285                serde_json::from_str::<serde_json::Value>(json_str)
286                    .map_err(|_| BuildError::Security("Invalid JSON in FFI input".to_string()))?;
287            }
288            FfiDataType::Binary => {
289                }
291        }
292
293        Ok(())
294    }
295
296    fn validate_xml_structure(&self, xml: &str) -> Result<(), BuildError> {
298        let mut reader = quick_xml::Reader::from_str(xml);
299        reader.config_mut().expand_empty_elements = false;
300
301        let mut buf = Vec::new();
302        let mut depth: i32 = 0;
303
304        loop {
305            match reader.read_event_into(&mut buf) {
306                Ok(quick_xml::events::Event::Start(_)) => {
307                    depth += 1;
308                    if depth > 100 {
309                        return Err(BuildError::Security(
311                            "XML depth limit exceeded in FFI input".to_string(),
312                        ));
313                    }
314                }
315                Ok(quick_xml::events::Event::End(_)) => {
316                    depth = depth.saturating_sub(1);
317                }
318                Ok(quick_xml::events::Event::Eof) => break,
319                Ok(_) => {}
320                Err(e) => {
321                    return Err(BuildError::Security(format!(
322                        "Invalid XML structure in FFI input: {}",
323                        e
324                    )));
325                }
326            }
327            buf.clear();
328        }
329
330        Ok(())
331    }
332}
333
334#[derive(Debug, Clone)]
336pub struct ApiSecurityConfig {
337    pub enabled: bool,
339    pub max_concurrent_requests: u32,
341    pub request_timeout_seconds: u64,
343    pub detailed_errors: bool,
345    pub enable_cors: bool,
347    pub allowed_origins: Vec<String>,
349}
350
351impl Default for ApiSecurityConfig {
352    fn default() -> Self {
353        Self {
354            enabled: true,
355            max_concurrent_requests: 10,
356            request_timeout_seconds: 30,
357            detailed_errors: false, enable_cors: false,
359            allowed_origins: vec!["https://localhost".to_string()],
360        }
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_api_security_manager() {
370        let config = SecurityConfig::default();
371        let mut manager = ApiSecurityManager::new(config);
372
373        assert!(manager.validate_request("parse", "user1", 1000).is_ok());
375
376        let result = manager.validate_request("parse", "user1", 200_000_000);
378        assert!(
379            result.is_err(),
380            "Expected oversized payload to be rejected, but got: {:?}",
381            result
382        );
383    }
384
385    #[test]
386    fn test_batch_operation_monitor() {
387        let config = SecurityConfig {
388            max_requests_per_minute: 3,
389            ..SecurityConfig::default()
390        };
391        let mut monitor = BatchOperationMonitor::new(config);
392
393        assert!(monitor.track_operation("user1", "parse").is_ok());
395        assert!(monitor.track_operation("user1", "build").is_ok());
396        assert!(monitor.track_operation("user1", "validate").is_ok());
397
398        assert!(monitor.track_operation("user1", "parse").is_err());
400
401        assert!(monitor.track_operation("user2", "parse").is_ok());
403
404        let stats = monitor.get_stats("user1").unwrap();
406        assert_eq!(stats.total_operations, 3);
407        assert_eq!(stats.unique_operations, 3);
408    }
409
410    #[test]
411    fn test_ffi_validator() {
412        let config = SecurityConfig::default();
413        let validator = FfiValidator::new(config);
414
415        let valid_string = "Hello, world!".as_bytes();
417        assert!(validator
418            .validate_input(valid_string, FfiDataType::Utf8String)
419            .is_ok());
420
421        let valid_xml = "<root><child>content</child></root>".as_bytes();
423        assert!(validator
424            .validate_input(valid_xml, FfiDataType::Xml)
425            .is_ok());
426
427        let valid_json = r#"{"key": "value"}"#.as_bytes();
429        assert!(validator
430            .validate_input(valid_json, FfiDataType::Json)
431            .is_ok());
432
433        let invalid_utf8 = &[0xff, 0xfe, 0xfd];
435        assert!(validator
436            .validate_input(invalid_utf8, FfiDataType::Utf8String)
437            .is_err());
438
439        let invalid_json = "{broken json".as_bytes();
441        assert!(validator
442            .validate_input(invalid_json, FfiDataType::Json)
443            .is_err());
444    }
445
446    #[test]
447    fn test_wasm_security_headers() {
448        let config = SecurityConfig::default();
449        let manager = ApiSecurityManager::new(config);
450
451        let headers = manager.get_wasm_security_headers();
452
453        assert!(headers.contains_key("Content-Security-Policy"));
455        assert!(headers.contains_key("X-Content-Type-Options"));
456        assert!(headers.contains_key("X-Frame-Options"));
457        assert!(headers.contains_key("X-XSS-Protection"));
458
459        assert_eq!(headers.get("X-Content-Type-Options").unwrap(), "nosniff");
461        assert_eq!(headers.get("X-Frame-Options").unwrap(), "DENY");
462    }
463
464    #[test]
465    fn test_secure_error_response() {
466        let config = SecurityConfig::default();
467        let manager = ApiSecurityManager::new(config);
468
469        let error = BuildError::Security("Internal security details".to_string());
470        let response = manager.create_secure_error_response(&error, "req-123");
471
472        assert!(!response.contains("Internal security details"));
474        assert!(response.contains("Security validation failed"));
475        assert!(response.contains("req-123"));
476        assert!(response.contains("error"));
477    }
478}