1use crate::config::ConfigError;
4use crate::security::compression_bomb::CompressionBombConfig;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct SecurityConfig {
11 pub json: JsonLimits,
13
14 pub buffers: BufferLimits,
16
17 pub network: NetworkLimits,
19
20 pub sessions: SessionLimits,
22}
23
24pub const MAX_SAFE_JSON_DEPTH: usize = 512;
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JsonLimits {
36 pub max_input_size: usize,
38
39 pub max_depth: usize,
41
42 pub max_object_keys: usize,
44
45 pub max_array_length: usize,
47
48 pub max_string_length: usize,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct BufferLimits {
55 pub max_buffer_size: usize,
57
58 pub max_pool_size: usize,
60
61 pub max_total_memory: usize,
63
64 pub buffer_ttl_secs: u64,
66
67 pub max_buffers_per_bucket: usize,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct NetworkLimits {
74 pub max_websocket_frame_size: usize,
76
77 pub max_concurrent_connections: usize,
79
80 pub connection_timeout_secs: u64,
82
83 pub max_requests_per_second: u32,
85
86 pub max_http_payload_size: usize,
88
89 pub rate_limiting: RateLimitingConfig,
91
92 pub compression_bomb: CompressionBombConfig,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct RateLimitingConfig {
99 pub max_requests_per_window: u32,
101
102 pub window_duration_secs: u64,
104
105 pub max_connections_per_ip: usize,
107
108 pub max_messages_per_second: u32,
110
111 pub burst_allowance: u32,
113
114 pub enabled: bool,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct SessionLimits {
121 pub max_session_id_length: usize,
123
124 pub min_session_id_length: usize,
126
127 pub max_streams_per_session: usize,
129
130 pub session_timeout_secs: u64,
132
133 pub max_session_data_size: usize,
135}
136
137impl Default for JsonLimits {
138 fn default() -> Self {
139 Self {
140 max_input_size: 100 * 1024 * 1024, max_depth: 64,
142 max_object_keys: 10_000,
143 max_array_length: 1_000_000,
144 max_string_length: 10 * 1024 * 1024, }
146 }
147}
148
149impl Default for BufferLimits {
150 fn default() -> Self {
151 Self {
152 max_buffer_size: 256 * 1024 * 1024, max_pool_size: 1000,
154 max_total_memory: 512 * 1024 * 1024, buffer_ttl_secs: 300, max_buffers_per_bucket: 50,
157 }
158 }
159}
160
161impl Default for NetworkLimits {
162 fn default() -> Self {
163 Self {
164 max_websocket_frame_size: 16 * 1024 * 1024, max_concurrent_connections: 10_000,
166 connection_timeout_secs: 30,
167 max_requests_per_second: 100,
168 max_http_payload_size: 50 * 1024 * 1024, rate_limiting: RateLimitingConfig::default(),
170 compression_bomb: CompressionBombConfig::default(),
171 }
172 }
173}
174
175impl Default for RateLimitingConfig {
176 fn default() -> Self {
177 Self {
178 max_requests_per_window: 100,
179 window_duration_secs: 60,
180 max_connections_per_ip: 10,
181 max_messages_per_second: 30,
182 burst_allowance: 5,
183 enabled: true,
184 }
185 }
186}
187
188impl Default for SessionLimits {
189 fn default() -> Self {
190 Self {
191 max_session_id_length: 128,
192 min_session_id_length: 8,
193 max_streams_per_session: 100,
194 session_timeout_secs: 3600, max_session_data_size: 100 * 1024 * 1024, }
197 }
198}
199
200impl SecurityConfig {
201 pub fn validate(&self) -> Result<(), ConfigError> {
220 let s = "security.sessions";
221
222 if self.sessions.min_session_id_length > self.sessions.max_session_id_length {
223 return Err(ConfigError::InconsistentBounds {
224 section: s,
225 message: "min_session_id_length must be <= max_session_id_length",
226 });
227 }
228
229 if self.json.max_depth > MAX_SAFE_JSON_DEPTH {
230 return Err(ConfigError::InconsistentBounds {
231 section: "security.json",
232 message: "max_depth exceeds MAX_SAFE_JSON_DEPTH (sonic-rs has no internal \
233 recursion limit; excessive depth risks stack exhaustion)",
234 });
235 }
236
237 macro_rules! must_be_positive {
238 ($section:expr, $field:expr, $value:expr) => {
239 if $value == 0 {
240 return Err(ConfigError::MustBePositive {
241 section: $section,
242 field: $field,
243 });
244 }
245 };
246 }
247
248 must_be_positive!("security.json", "max_input_size", self.json.max_input_size);
249 must_be_positive!("security.json", "max_depth", self.json.max_depth);
250 must_be_positive!(
251 "security.json",
252 "max_string_length",
253 self.json.max_string_length
254 );
255 must_be_positive!(
256 "security.buffers",
257 "max_buffer_size",
258 self.buffers.max_buffer_size
259 );
260 must_be_positive!(
261 "security.buffers",
262 "max_total_memory",
263 self.buffers.max_total_memory
264 );
265 must_be_positive!(
266 "security.network",
267 "max_websocket_frame_size",
268 self.network.max_websocket_frame_size
269 );
270 must_be_positive!(
271 "security.network",
272 "max_http_payload_size",
273 self.network.max_http_payload_size
274 );
275 must_be_positive!(
276 "security.sessions",
277 "max_session_id_length",
278 self.sessions.max_session_id_length
279 );
280 must_be_positive!(
281 "security.sessions",
282 "min_session_id_length",
283 self.sessions.min_session_id_length
284 );
285 must_be_positive!(
286 "security.sessions",
287 "max_session_data_size",
288 self.sessions.max_session_data_size
289 );
290
291 Ok(())
292 }
293
294 pub fn high_throughput() -> Self {
296 Self {
297 json: JsonLimits {
298 max_input_size: 500 * 1024 * 1024, max_depth: 128,
300 max_object_keys: 50_000,
301 max_array_length: 5_000_000,
302 max_string_length: 50 * 1024 * 1024, },
304 buffers: BufferLimits {
305 max_buffer_size: 1024 * 1024 * 1024, max_pool_size: 5000,
307 max_total_memory: 2 * 1024 * 1024 * 1024, buffer_ttl_secs: 600, max_buffers_per_bucket: 200,
310 },
311 network: NetworkLimits {
312 max_websocket_frame_size: 100 * 1024 * 1024, max_concurrent_connections: 50_000,
314 connection_timeout_secs: 60,
315 max_requests_per_second: 1000,
316 max_http_payload_size: 200 * 1024 * 1024, rate_limiting: RateLimitingConfig {
318 max_requests_per_window: 1000,
319 window_duration_secs: 60,
320 max_connections_per_ip: 50,
321 max_messages_per_second: 100,
322 burst_allowance: 20,
323 enabled: true,
324 },
325 compression_bomb: CompressionBombConfig::high_throughput(),
326 },
327 sessions: SessionLimits {
328 max_session_id_length: 256,
329 min_session_id_length: 16,
330 max_streams_per_session: 1000,
331 session_timeout_secs: 7200, max_session_data_size: 500 * 1024 * 1024, },
334 }
335 }
336
337 pub fn low_memory() -> Self {
339 Self {
340 json: JsonLimits {
341 max_input_size: 10 * 1024 * 1024, max_depth: 32,
343 max_object_keys: 1_000,
344 max_array_length: 100_000,
345 max_string_length: 1024 * 1024, },
347 buffers: BufferLimits {
348 max_buffer_size: 10 * 1024 * 1024, max_pool_size: 100,
350 max_total_memory: 50 * 1024 * 1024, buffer_ttl_secs: 60, max_buffers_per_bucket: 10,
353 },
354 network: NetworkLimits {
355 max_websocket_frame_size: 1024 * 1024, max_concurrent_connections: 1_000,
357 connection_timeout_secs: 15,
358 max_requests_per_second: 10,
359 max_http_payload_size: 5 * 1024 * 1024, rate_limiting: RateLimitingConfig {
361 max_requests_per_window: 20,
362 window_duration_secs: 60,
363 max_connections_per_ip: 2,
364 max_messages_per_second: 5,
365 burst_allowance: 2,
366 enabled: true,
367 },
368 compression_bomb: CompressionBombConfig::low_memory(),
369 },
370 sessions: SessionLimits {
371 max_session_id_length: 64,
372 min_session_id_length: 8,
373 max_streams_per_session: 10,
374 session_timeout_secs: 900, max_session_data_size: 10 * 1024 * 1024, },
377 }
378 }
379
380 pub fn development() -> Self {
382 Self {
383 json: JsonLimits {
384 max_input_size: 50 * 1024 * 1024, max_depth: 64,
386 max_object_keys: 5_000,
387 max_array_length: 500_000,
388 max_string_length: 5 * 1024 * 1024, },
390 buffers: BufferLimits {
391 max_buffer_size: 100 * 1024 * 1024, max_pool_size: 500,
393 max_total_memory: 200 * 1024 * 1024, buffer_ttl_secs: 120, max_buffers_per_bucket: 25,
396 },
397 network: NetworkLimits {
398 max_websocket_frame_size: 10 * 1024 * 1024, max_concurrent_connections: 1_000,
400 connection_timeout_secs: 30,
401 max_requests_per_second: 50,
402 max_http_payload_size: 25 * 1024 * 1024, rate_limiting: RateLimitingConfig {
404 max_requests_per_window: 200,
405 window_duration_secs: 60,
406 max_connections_per_ip: 20,
407 max_messages_per_second: 50,
408 burst_allowance: 10,
409 enabled: true,
410 },
411 compression_bomb: CompressionBombConfig::default(),
412 },
413 sessions: SessionLimits {
414 max_session_id_length: 128,
415 min_session_id_length: 8,
416 max_streams_per_session: 50,
417 session_timeout_secs: 1800, max_session_data_size: 50 * 1024 * 1024, },
420 }
421 }
422
423 pub fn buffer_ttl(&self) -> Duration {
425 Duration::from_secs(self.buffers.buffer_ttl_secs)
426 }
427
428 pub fn connection_timeout(&self) -> Duration {
430 Duration::from_secs(self.network.connection_timeout_secs)
431 }
432
433 pub fn session_timeout(&self) -> Duration {
435 Duration::from_secs(self.sessions.session_timeout_secs)
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use crate::config::ConfigError;
443
444 #[test]
445 fn test_default_security_config() {
446 let config = SecurityConfig::default();
447
448 assert!(config.json.max_input_size > 0);
450 assert!(config.buffers.max_buffer_size > 0);
451 assert!(config.network.max_concurrent_connections > 0);
452 assert!(config.sessions.max_session_id_length >= config.sessions.min_session_id_length);
453 }
454
455 #[test]
456 fn test_security_config_default_validates() {
457 SecurityConfig::default()
458 .validate()
459 .expect("SecurityConfig::default() must be valid");
460 }
461
462 #[test]
463 fn test_rejects_min_session_id_length_greater_than_max() {
464 let mut config = SecurityConfig::default();
465 config.sessions.min_session_id_length = 200;
466 config.sessions.max_session_id_length = 100;
467 let err = config.validate().unwrap_err();
468 assert!(matches!(
469 err,
470 ConfigError::InconsistentBounds {
471 section: "security.sessions",
472 ..
473 }
474 ));
475 }
476
477 #[test]
478 fn test_rejects_max_depth_above_hard_ceiling() {
479 let mut config = SecurityConfig::default();
480 config.json.max_depth = MAX_SAFE_JSON_DEPTH + 1;
481 let err = config.validate().unwrap_err();
482 assert!(matches!(
483 err,
484 ConfigError::InconsistentBounds {
485 section: "security.json",
486 ..
487 }
488 ));
489 }
490
491 #[test]
492 fn test_max_depth_exactly_at_hard_ceiling_validates() {
493 let mut config = SecurityConfig::default();
494 config.json.max_depth = MAX_SAFE_JSON_DEPTH;
495 config.validate().expect("boundary value must be valid");
496 }
497
498 #[test]
499 fn test_max_deserialize_depth_matches_domain_guard_defaults() {
500 let msg = "pjs-domain's MAX_DESERIALIZE_DEPTH (a hard ceiling enforced by \
506 JsonData's Deserialize impl) and pjs-core's JsonLimits::max_depth \
507 (a configurable knob enforced by SecurityValidator) should default \
508 to the same value, so a default JsonLimits does not admit input \
509 that JsonData::deserialize would then reject";
510
511 assert_eq!(
512 pjson_rs_domain::MAX_DESERIALIZE_DEPTH,
513 SecurityConfig::default().json.max_depth,
514 "{msg}"
515 );
516 assert_eq!(
517 pjson_rs_domain::MAX_DESERIALIZE_DEPTH,
518 SecurityConfig::development().json.max_depth,
519 "{msg}"
520 );
521 }
522
523 #[cfg(feature = "partial-parse")]
524 #[test]
525 fn test_jiter_config_default_max_depth_matches_domain_guard() {
526 assert_eq!(
527 pjson_rs_domain::MAX_DESERIALIZE_DEPTH,
528 crate::parser::partial::JiterConfig::default().max_depth,
529 "JiterPartialParser's default max_depth should match pjs-domain's \
530 MAX_DESERIALIZE_DEPTH hard ceiling"
531 );
532 }
533
534 #[test]
535 fn test_high_throughput_config() {
536 let config = SecurityConfig::high_throughput();
537 let default = SecurityConfig::default();
538
539 assert!(config.json.max_input_size >= default.json.max_input_size);
541 assert!(config.buffers.max_total_memory >= default.buffers.max_total_memory);
542 assert!(
543 config.network.max_concurrent_connections >= default.network.max_concurrent_connections
544 );
545 }
546
547 #[test]
548 fn test_low_memory_config() {
549 let config = SecurityConfig::low_memory();
550 let default = SecurityConfig::default();
551
552 assert!(config.json.max_input_size <= default.json.max_input_size);
554 assert!(config.buffers.max_total_memory <= default.buffers.max_total_memory);
555 assert!(config.buffers.max_buffers_per_bucket <= default.buffers.max_buffers_per_bucket);
556 }
557
558 #[test]
559 fn test_duration_conversions() {
560 let config = SecurityConfig::default();
561
562 assert!(config.buffer_ttl().as_secs() > 0);
563 assert!(config.connection_timeout().as_secs() > 0);
564 assert!(config.session_timeout().as_secs() > 0);
565 }
566}