Skip to main content

pjson_rs/config/
security.rs

1//! Security configuration and limits
2
3use crate::config::ConfigError;
4use crate::security::compression_bomb::CompressionBombConfig;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8/// Security configuration for the PJS system
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct SecurityConfig {
11    /// JSON processing limits
12    pub json: JsonLimits,
13
14    /// Buffer management limits
15    pub buffers: BufferLimits,
16
17    /// Network and connection limits
18    pub network: NetworkLimits,
19
20    /// Session management limits
21    pub sessions: SessionLimits,
22}
23
24/// JSON processing security limits
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct JsonLimits {
27    /// Maximum JSON input size in bytes
28    pub max_input_size: usize,
29
30    /// Maximum JSON nesting depth
31    pub max_depth: usize,
32
33    /// Maximum number of keys in a JSON object
34    pub max_object_keys: usize,
35
36    /// Maximum array length
37    pub max_array_length: usize,
38
39    /// Maximum string length in JSON
40    pub max_string_length: usize,
41}
42
43/// Buffer management security limits
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct BufferLimits {
46    /// Maximum individual buffer size
47    pub max_buffer_size: usize,
48
49    /// Maximum number of buffers in pool
50    pub max_pool_size: usize,
51
52    /// Maximum total memory for all buffer pools
53    pub max_total_memory: usize,
54
55    /// Buffer time-to-live before cleanup
56    pub buffer_ttl_secs: u64,
57
58    /// Maximum buffers per size bucket
59    pub max_buffers_per_bucket: usize,
60}
61
62/// Network and connection security limits
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct NetworkLimits {
65    /// Maximum WebSocket frame size
66    pub max_websocket_frame_size: usize,
67
68    /// Maximum number of concurrent connections
69    pub max_concurrent_connections: usize,
70
71    /// Connection timeout in seconds
72    pub connection_timeout_secs: u64,
73
74    /// Maximum request rate per connection (requests per second)
75    pub max_requests_per_second: u32,
76
77    /// Maximum payload size for HTTP requests
78    pub max_http_payload_size: usize,
79
80    /// Rate limiting configuration
81    pub rate_limiting: RateLimitingConfig,
82
83    /// Compression bomb protection configuration
84    pub compression_bomb: CompressionBombConfig,
85}
86
87/// Rate limiting configuration for DoS protection
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct RateLimitingConfig {
90    /// Maximum requests per time window per IP
91    pub max_requests_per_window: u32,
92
93    /// Time window for rate limiting in seconds
94    pub window_duration_secs: u64,
95
96    /// Maximum concurrent connections per IP
97    pub max_connections_per_ip: usize,
98
99    /// Maximum WebSocket messages per second per connection
100    pub max_messages_per_second: u32,
101
102    /// Burst allowance (extra messages above rate)
103    pub burst_allowance: u32,
104
105    /// Enable rate limiting
106    pub enabled: bool,
107}
108
109/// Session management security limits
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct SessionLimits {
112    /// Maximum session ID length
113    pub max_session_id_length: usize,
114
115    /// Minimum session ID length
116    pub min_session_id_length: usize,
117
118    /// Maximum streams per session
119    pub max_streams_per_session: usize,
120
121    /// Session idle timeout in seconds
122    pub session_timeout_secs: u64,
123
124    /// Maximum session data size
125    pub max_session_data_size: usize,
126}
127
128impl Default for JsonLimits {
129    fn default() -> Self {
130        Self {
131            max_input_size: 100 * 1024 * 1024, // 100MB
132            max_depth: 64,
133            max_object_keys: 10_000,
134            max_array_length: 1_000_000,
135            max_string_length: 10 * 1024 * 1024, // 10MB
136        }
137    }
138}
139
140impl Default for BufferLimits {
141    fn default() -> Self {
142        Self {
143            max_buffer_size: 256 * 1024 * 1024, // 256MB
144            max_pool_size: 1000,
145            max_total_memory: 512 * 1024 * 1024, // 512MB
146            buffer_ttl_secs: 300,                // 5 minutes
147            max_buffers_per_bucket: 50,
148        }
149    }
150}
151
152impl Default for NetworkLimits {
153    fn default() -> Self {
154        Self {
155            max_websocket_frame_size: 16 * 1024 * 1024, // 16MB
156            max_concurrent_connections: 10_000,
157            connection_timeout_secs: 30,
158            max_requests_per_second: 100,
159            max_http_payload_size: 50 * 1024 * 1024, // 50MB
160            rate_limiting: RateLimitingConfig::default(),
161            compression_bomb: CompressionBombConfig::default(),
162        }
163    }
164}
165
166impl Default for RateLimitingConfig {
167    fn default() -> Self {
168        Self {
169            max_requests_per_window: 100,
170            window_duration_secs: 60,
171            max_connections_per_ip: 10,
172            max_messages_per_second: 30,
173            burst_allowance: 5,
174            enabled: true,
175        }
176    }
177}
178
179impl Default for SessionLimits {
180    fn default() -> Self {
181        Self {
182            max_session_id_length: 128,
183            min_session_id_length: 8,
184            max_streams_per_session: 100,
185            session_timeout_secs: 3600,               // 1 hour
186            max_session_data_size: 100 * 1024 * 1024, // 100MB
187        }
188    }
189}
190
191impl SecurityConfig {
192    /// Validate all security configuration values.
193    ///
194    /// Returns `Err` if any invariant is violated (e.g. `min_session_id_length`
195    /// exceeds `max_session_id_length`, or a size limit is zero).
196    ///
197    /// # Errors
198    ///
199    /// Returns [`ConfigError::MustBePositive`] when a size field is zero.
200    /// Returns [`ConfigError::InconsistentBounds`] when min > max for session
201    /// ID length.
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// use pjson_rs::config::SecurityConfig;
207    ///
208    /// SecurityConfig::default().validate().expect("defaults are valid");
209    /// ```
210    pub fn validate(&self) -> Result<(), ConfigError> {
211        let s = "security.sessions";
212
213        if self.sessions.min_session_id_length > self.sessions.max_session_id_length {
214            return Err(ConfigError::InconsistentBounds {
215                section: s,
216                message: "min_session_id_length must be <= max_session_id_length",
217            });
218        }
219
220        macro_rules! must_be_positive {
221            ($section:expr, $field:expr, $value:expr) => {
222                if $value == 0 {
223                    return Err(ConfigError::MustBePositive {
224                        section: $section,
225                        field: $field,
226                    });
227                }
228            };
229        }
230
231        must_be_positive!("security.json", "max_input_size", self.json.max_input_size);
232        must_be_positive!("security.json", "max_depth", self.json.max_depth);
233        must_be_positive!(
234            "security.json",
235            "max_string_length",
236            self.json.max_string_length
237        );
238        must_be_positive!(
239            "security.buffers",
240            "max_buffer_size",
241            self.buffers.max_buffer_size
242        );
243        must_be_positive!(
244            "security.buffers",
245            "max_total_memory",
246            self.buffers.max_total_memory
247        );
248        must_be_positive!(
249            "security.network",
250            "max_websocket_frame_size",
251            self.network.max_websocket_frame_size
252        );
253        must_be_positive!(
254            "security.network",
255            "max_http_payload_size",
256            self.network.max_http_payload_size
257        );
258        must_be_positive!(
259            "security.sessions",
260            "max_session_id_length",
261            self.sessions.max_session_id_length
262        );
263        must_be_positive!(
264            "security.sessions",
265            "min_session_id_length",
266            self.sessions.min_session_id_length
267        );
268        must_be_positive!(
269            "security.sessions",
270            "max_session_data_size",
271            self.sessions.max_session_data_size
272        );
273
274        Ok(())
275    }
276
277    /// Create a configuration optimized for high-throughput scenarios
278    pub fn high_throughput() -> Self {
279        Self {
280            json: JsonLimits {
281                max_input_size: 500 * 1024 * 1024, // 500MB
282                max_depth: 128,
283                max_object_keys: 50_000,
284                max_array_length: 5_000_000,
285                max_string_length: 50 * 1024 * 1024, // 50MB
286            },
287            buffers: BufferLimits {
288                max_buffer_size: 1024 * 1024 * 1024, // 1GB
289                max_pool_size: 5000,
290                max_total_memory: 2 * 1024 * 1024 * 1024, // 2GB
291                buffer_ttl_secs: 600,                     // 10 minutes
292                max_buffers_per_bucket: 200,
293            },
294            network: NetworkLimits {
295                max_websocket_frame_size: 100 * 1024 * 1024, // 100MB
296                max_concurrent_connections: 50_000,
297                connection_timeout_secs: 60,
298                max_requests_per_second: 1000,
299                max_http_payload_size: 200 * 1024 * 1024, // 200MB
300                rate_limiting: RateLimitingConfig {
301                    max_requests_per_window: 1000,
302                    window_duration_secs: 60,
303                    max_connections_per_ip: 50,
304                    max_messages_per_second: 100,
305                    burst_allowance: 20,
306                    enabled: true,
307                },
308                compression_bomb: CompressionBombConfig::high_throughput(),
309            },
310            sessions: SessionLimits {
311                max_session_id_length: 256,
312                min_session_id_length: 16,
313                max_streams_per_session: 1000,
314                session_timeout_secs: 7200,               // 2 hours
315                max_session_data_size: 500 * 1024 * 1024, // 500MB
316            },
317        }
318    }
319
320    /// Create a configuration optimized for low-memory environments
321    pub fn low_memory() -> Self {
322        Self {
323            json: JsonLimits {
324                max_input_size: 10 * 1024 * 1024, // 10MB
325                max_depth: 32,
326                max_object_keys: 1_000,
327                max_array_length: 100_000,
328                max_string_length: 1024 * 1024, // 1MB
329            },
330            buffers: BufferLimits {
331                max_buffer_size: 10 * 1024 * 1024, // 10MB
332                max_pool_size: 100,
333                max_total_memory: 50 * 1024 * 1024, // 50MB
334                buffer_ttl_secs: 60,                // 1 minute
335                max_buffers_per_bucket: 10,
336            },
337            network: NetworkLimits {
338                max_websocket_frame_size: 1024 * 1024, // 1MB
339                max_concurrent_connections: 1_000,
340                connection_timeout_secs: 15,
341                max_requests_per_second: 10,
342                max_http_payload_size: 5 * 1024 * 1024, // 5MB
343                rate_limiting: RateLimitingConfig {
344                    max_requests_per_window: 20,
345                    window_duration_secs: 60,
346                    max_connections_per_ip: 2,
347                    max_messages_per_second: 5,
348                    burst_allowance: 2,
349                    enabled: true,
350                },
351                compression_bomb: CompressionBombConfig::low_memory(),
352            },
353            sessions: SessionLimits {
354                max_session_id_length: 64,
355                min_session_id_length: 8,
356                max_streams_per_session: 10,
357                session_timeout_secs: 900,               // 15 minutes
358                max_session_data_size: 10 * 1024 * 1024, // 10MB
359            },
360        }
361    }
362
363    /// Create a configuration optimized for development/testing
364    pub fn development() -> Self {
365        Self {
366            json: JsonLimits {
367                max_input_size: 50 * 1024 * 1024, // 50MB
368                max_depth: 64,
369                max_object_keys: 5_000,
370                max_array_length: 500_000,
371                max_string_length: 5 * 1024 * 1024, // 5MB
372            },
373            buffers: BufferLimits {
374                max_buffer_size: 100 * 1024 * 1024, // 100MB
375                max_pool_size: 500,
376                max_total_memory: 200 * 1024 * 1024, // 200MB
377                buffer_ttl_secs: 120,                // 2 minutes
378                max_buffers_per_bucket: 25,
379            },
380            network: NetworkLimits {
381                max_websocket_frame_size: 10 * 1024 * 1024, // 10MB
382                max_concurrent_connections: 1_000,
383                connection_timeout_secs: 30,
384                max_requests_per_second: 50,
385                max_http_payload_size: 25 * 1024 * 1024, // 25MB
386                rate_limiting: RateLimitingConfig {
387                    max_requests_per_window: 200,
388                    window_duration_secs: 60,
389                    max_connections_per_ip: 20,
390                    max_messages_per_second: 50,
391                    burst_allowance: 10,
392                    enabled: true,
393                },
394                compression_bomb: CompressionBombConfig::default(),
395            },
396            sessions: SessionLimits {
397                max_session_id_length: 128,
398                min_session_id_length: 8,
399                max_streams_per_session: 50,
400                session_timeout_secs: 1800,              // 30 minutes
401                max_session_data_size: 50 * 1024 * 1024, // 50MB
402            },
403        }
404    }
405
406    /// Get buffer TTL as Duration
407    pub fn buffer_ttl(&self) -> Duration {
408        Duration::from_secs(self.buffers.buffer_ttl_secs)
409    }
410
411    /// Get connection timeout as Duration
412    pub fn connection_timeout(&self) -> Duration {
413        Duration::from_secs(self.network.connection_timeout_secs)
414    }
415
416    /// Get session timeout as Duration
417    pub fn session_timeout(&self) -> Duration {
418        Duration::from_secs(self.sessions.session_timeout_secs)
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425    use crate::config::ConfigError;
426
427    #[test]
428    fn test_default_security_config() {
429        let config = SecurityConfig::default();
430
431        // Test reasonable defaults
432        assert!(config.json.max_input_size > 0);
433        assert!(config.buffers.max_buffer_size > 0);
434        assert!(config.network.max_concurrent_connections > 0);
435        assert!(config.sessions.max_session_id_length >= config.sessions.min_session_id_length);
436    }
437
438    #[test]
439    fn test_security_config_default_validates() {
440        SecurityConfig::default()
441            .validate()
442            .expect("SecurityConfig::default() must be valid");
443    }
444
445    #[test]
446    fn test_rejects_min_session_id_length_greater_than_max() {
447        let mut config = SecurityConfig::default();
448        config.sessions.min_session_id_length = 200;
449        config.sessions.max_session_id_length = 100;
450        let err = config.validate().unwrap_err();
451        assert!(matches!(
452            err,
453            ConfigError::InconsistentBounds {
454                section: "security.sessions",
455                ..
456            }
457        ));
458    }
459
460    #[test]
461    fn test_high_throughput_config() {
462        let config = SecurityConfig::high_throughput();
463        let default = SecurityConfig::default();
464
465        // High throughput should have higher limits
466        assert!(config.json.max_input_size >= default.json.max_input_size);
467        assert!(config.buffers.max_total_memory >= default.buffers.max_total_memory);
468        assert!(
469            config.network.max_concurrent_connections >= default.network.max_concurrent_connections
470        );
471    }
472
473    #[test]
474    fn test_low_memory_config() {
475        let config = SecurityConfig::low_memory();
476        let default = SecurityConfig::default();
477
478        // Low memory should have lower limits
479        assert!(config.json.max_input_size <= default.json.max_input_size);
480        assert!(config.buffers.max_total_memory <= default.buffers.max_total_memory);
481        assert!(config.buffers.max_buffers_per_bucket <= default.buffers.max_buffers_per_bucket);
482    }
483
484    #[test]
485    fn test_duration_conversions() {
486        let config = SecurityConfig::default();
487
488        assert!(config.buffer_ttl().as_secs() > 0);
489        assert!(config.connection_timeout().as_secs() > 0);
490        assert!(config.session_timeout().as_secs() > 0);
491    }
492}