1#![allow(deprecated)] use std::time::Duration;
13
14use reinhardt_core::macros::settings;
15use serde::{Deserialize, Serialize};
16
17use crate::connection::ConnectionConfig;
18use crate::origin::{OriginPolicy, OriginValidationConfig};
19use crate::reconnection::ReconnectionConfig;
20use crate::throttling::WebSocketRateLimitConfig;
21
22#[cfg(feature = "redis-channel")]
23use crate::redis_channel::RedisConfig;
24
25fn default_idle_timeout_secs() -> u64 {
28 300
29}
30fn default_handshake_timeout_secs() -> u64 {
31 10
32}
33fn default_cleanup_interval_secs() -> u64 {
34 30
35}
36fn default_reconnect_max_attempts() -> Option<u32> {
37 Some(10)
38}
39fn default_reconnect_initial_delay_secs() -> u64 {
40 1
41}
42fn default_reconnect_max_delay_secs() -> u64 {
43 300
44}
45fn default_reconnect_backoff_multiplier() -> f64 {
46 2.0
47}
48fn default_reconnect_jitter_factor() -> f64 {
49 0.1
50}
51fn default_reject_missing_origin() -> bool {
52 true
53}
54fn default_rate_max_connections_per_window() -> usize {
55 20
56}
57fn default_rate_connection_window_secs() -> u64 {
58 60
59}
60fn default_rate_max_concurrent_connections_per_ip() -> usize {
61 10
62}
63fn default_rate_max_messages_per_window() -> usize {
64 100
65}
66fn default_rate_message_window_secs() -> u64 {
67 60
68}
69
70#[cfg(feature = "redis-channel")]
71fn default_redis_url() -> String {
72 "redis://127.0.0.1:6379".to_string()
73}
74#[cfg(feature = "redis-channel")]
75fn default_redis_channel_prefix() -> String {
76 "ws:channel:".to_string()
77}
78#[cfg(feature = "redis-channel")]
79fn default_redis_group_prefix() -> String {
80 "ws:group:".to_string()
81}
82#[cfg(feature = "redis-channel")]
83fn default_redis_message_expiry() -> u64 {
84 60
85}
86#[cfg(feature = "redis-channel")]
87fn default_redis_require_auth() -> bool {
88 true
89}
90
91#[settings(fragment = true, section = "ws_connection")]
97#[non_exhaustive]
98#[derive(Clone, Debug, Serialize, Deserialize)]
99pub struct ConnectionSettings {
100 #[serde(default = "default_idle_timeout_secs")]
102 pub idle_timeout_secs: u64,
103 #[serde(default = "default_handshake_timeout_secs")]
105 pub handshake_timeout_secs: u64,
106 #[serde(default = "default_cleanup_interval_secs")]
108 pub cleanup_interval_secs: u64,
109 #[serde(default)]
111 pub max_connections: Option<usize>,
112}
113
114impl Default for ConnectionSettings {
115 fn default() -> Self {
116 Self {
117 idle_timeout_secs: default_idle_timeout_secs(),
118 handshake_timeout_secs: default_handshake_timeout_secs(),
119 cleanup_interval_secs: default_cleanup_interval_secs(),
120 max_connections: None,
121 }
122 }
123}
124
125impl From<&ConnectionSettings> for ConnectionConfig {
126 fn from(settings: &ConnectionSettings) -> Self {
127 ConnectionConfig::new()
129 .with_idle_timeout(Duration::from_secs(settings.idle_timeout_secs))
130 .with_handshake_timeout(Duration::from_secs(settings.handshake_timeout_secs))
131 .with_cleanup_interval(Duration::from_secs(settings.cleanup_interval_secs))
132 .with_max_connections(settings.max_connections)
133 }
134}
135
136pub fn create_connection_config_from_settings(settings: &ConnectionSettings) -> ConnectionConfig {
138 ConnectionConfig::from(settings)
139}
140
141#[settings(fragment = true, section = "ws_reconnection")]
147#[non_exhaustive]
148#[derive(Clone, Debug, Serialize, Deserialize)]
149pub struct ReconnectionSettings {
150 #[serde(default = "default_reconnect_max_attempts")]
152 pub max_attempts: Option<u32>,
153 #[serde(default = "default_reconnect_initial_delay_secs")]
155 pub initial_delay_secs: u64,
156 #[serde(default = "default_reconnect_max_delay_secs")]
158 pub max_delay_secs: u64,
159 #[serde(default = "default_reconnect_backoff_multiplier")]
161 pub backoff_multiplier: f64,
162 #[serde(default = "default_reconnect_jitter_factor")]
164 pub jitter_factor: f64,
165}
166
167impl Default for ReconnectionSettings {
168 fn default() -> Self {
169 Self {
170 max_attempts: default_reconnect_max_attempts(),
171 initial_delay_secs: default_reconnect_initial_delay_secs(),
172 max_delay_secs: default_reconnect_max_delay_secs(),
173 backoff_multiplier: default_reconnect_backoff_multiplier(),
174 jitter_factor: default_reconnect_jitter_factor(),
175 }
176 }
177}
178
179impl From<&ReconnectionSettings> for ReconnectionConfig {
180 fn from(settings: &ReconnectionSettings) -> Self {
181 ReconnectionConfig {
183 max_attempts: settings.max_attempts,
184 initial_delay: Duration::from_secs(settings.initial_delay_secs),
185 max_delay: Duration::from_secs(settings.max_delay_secs),
186 backoff_multiplier: settings.backoff_multiplier,
187 jitter_factor: settings.jitter_factor,
188 }
189 }
190}
191
192pub fn create_reconnection_config_from_settings(
194 settings: &ReconnectionSettings,
195) -> ReconnectionConfig {
196 ReconnectionConfig::from(settings)
197}
198
199#[derive(Clone, Debug, Serialize, Deserialize)]
206#[serde(rename_all = "snake_case", tag = "mode", content = "origins")]
207pub enum OriginPolicySettings {
208 AllowList(Vec<String>),
210 AllowAll,
212}
213
214impl Default for OriginPolicySettings {
215 fn default() -> Self {
216 OriginPolicySettings::AllowList(Vec::new())
219 }
220}
221
222impl From<&OriginPolicySettings> for OriginPolicy {
223 fn from(settings: &OriginPolicySettings) -> Self {
224 match settings {
225 OriginPolicySettings::AllowList(origins) => OriginPolicy::AllowList(origins.clone()),
226 OriginPolicySettings::AllowAll => OriginPolicy::AllowAll,
227 }
228 }
229}
230
231#[settings(fragment = true, section = "ws_origin")]
236#[non_exhaustive]
237#[derive(Clone, Debug, Serialize, Deserialize)]
238pub struct OriginValidationSettings {
239 #[serde(default)]
241 pub policy: OriginPolicySettings,
242 #[serde(default = "default_reject_missing_origin")]
244 pub reject_missing_origin: bool,
245}
246
247impl Default for OriginValidationSettings {
248 fn default() -> Self {
249 Self {
250 policy: OriginPolicySettings::default(),
251 reject_missing_origin: default_reject_missing_origin(),
252 }
253 }
254}
255
256impl From<&OriginValidationSettings> for OriginValidationConfig {
257 fn from(settings: &OriginValidationSettings) -> Self {
258 OriginValidationConfig {
259 policy: OriginPolicy::from(&settings.policy),
260 reject_missing_origin: settings.reject_missing_origin,
261 }
262 }
263}
264
265pub fn create_origin_validation_config_from_settings(
267 settings: &OriginValidationSettings,
268) -> OriginValidationConfig {
269 OriginValidationConfig::from(settings)
270}
271
272#[settings(fragment = true, section = "ws_rate_limit")]
278#[non_exhaustive]
279#[derive(Clone, Debug, Serialize, Deserialize)]
280pub struct RateLimitSettings {
281 #[serde(default = "default_rate_max_connections_per_window")]
283 pub max_connections_per_window: usize,
284 #[serde(default = "default_rate_connection_window_secs")]
286 pub connection_window_secs: u64,
287 #[serde(default = "default_rate_max_concurrent_connections_per_ip")]
289 pub max_concurrent_connections_per_ip: usize,
290 #[serde(default = "default_rate_max_messages_per_window")]
292 pub max_messages_per_window: usize,
293 #[serde(default = "default_rate_message_window_secs")]
295 pub message_window_secs: u64,
296}
297
298impl Default for RateLimitSettings {
299 fn default() -> Self {
300 Self {
301 max_connections_per_window: default_rate_max_connections_per_window(),
302 connection_window_secs: default_rate_connection_window_secs(),
303 max_concurrent_connections_per_ip: default_rate_max_concurrent_connections_per_ip(),
304 max_messages_per_window: default_rate_max_messages_per_window(),
305 message_window_secs: default_rate_message_window_secs(),
306 }
307 }
308}
309
310impl From<&RateLimitSettings> for WebSocketRateLimitConfig {
311 fn from(settings: &RateLimitSettings) -> Self {
312 WebSocketRateLimitConfig::default()
314 .with_max_connections_per_window(settings.max_connections_per_window)
315 .with_connection_window(Duration::from_secs(settings.connection_window_secs))
316 .with_max_concurrent_connections_per_ip(settings.max_concurrent_connections_per_ip)
317 .with_max_messages_per_window(settings.max_messages_per_window)
318 .with_message_window(Duration::from_secs(settings.message_window_secs))
319 }
320}
321
322pub fn create_rate_limit_config_from_settings(
324 settings: &RateLimitSettings,
325) -> WebSocketRateLimitConfig {
326 WebSocketRateLimitConfig::from(settings)
327}
328
329#[cfg(feature = "redis-channel")]
335#[settings(fragment = true, section = "ws_redis")]
336#[non_exhaustive]
337#[derive(Clone, Debug, Serialize, Deserialize)]
338pub struct RedisChannelSettings {
339 #[serde(default = "default_redis_url")]
341 pub url: String,
342 #[serde(default = "default_redis_channel_prefix")]
344 pub channel_prefix: String,
345 #[serde(default = "default_redis_group_prefix")]
347 pub group_prefix: String,
348 #[serde(default = "default_redis_message_expiry")]
350 pub message_expiry: u64,
351 #[serde(default)]
353 pub password: Option<String>,
354 #[serde(default)]
356 pub username: Option<String>,
357 #[serde(default)]
359 pub tls: bool,
360 #[serde(default = "default_redis_require_auth")]
362 pub require_auth: bool,
363}
364
365#[cfg(feature = "redis-channel")]
366impl Default for RedisChannelSettings {
367 fn default() -> Self {
368 Self {
369 url: default_redis_url(),
370 channel_prefix: default_redis_channel_prefix(),
371 group_prefix: default_redis_group_prefix(),
372 message_expiry: default_redis_message_expiry(),
373 password: None,
374 username: None,
375 tls: false,
376 require_auth: default_redis_require_auth(),
377 }
378 }
379}
380
381#[cfg(feature = "redis-channel")]
382impl From<&RedisChannelSettings> for RedisConfig {
383 fn from(settings: &RedisChannelSettings) -> Self {
384 RedisConfig {
386 url: settings.url.clone(),
387 channel_prefix: settings.channel_prefix.clone(),
388 group_prefix: settings.group_prefix.clone(),
389 message_expiry: settings.message_expiry,
390 password: settings.password.clone(),
391 username: settings.username.clone(),
392 tls: settings.tls,
393 require_auth: settings.require_auth,
394 }
395 }
396}
397
398#[cfg(feature = "redis-channel")]
400pub fn create_redis_config_from_settings(settings: &RedisChannelSettings) -> RedisConfig {
401 RedisConfig::from(settings)
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn connection_settings_default_converts() {
410 let settings = ConnectionSettings::default();
411 let config = ConnectionConfig::from(&settings);
412 assert_eq!(config.idle_timeout(), Duration::from_secs(300));
413 assert_eq!(config.handshake_timeout(), Duration::from_secs(10));
414 assert_eq!(config.cleanup_interval(), Duration::from_secs(30));
415 assert_eq!(config.max_connections(), None);
416 }
417
418 #[test]
419 fn reconnection_settings_default_converts() {
420 let settings = ReconnectionSettings::default();
421 let config = ReconnectionConfig::from(&settings);
422 assert_eq!(config.max_attempts, Some(10));
423 assert_eq!(config.initial_delay, Duration::from_secs(1));
424 assert_eq!(config.max_delay, Duration::from_secs(300));
425 assert_eq!(config.backoff_multiplier, 2.0);
426 assert_eq!(config.jitter_factor, 0.1);
427 }
428
429 #[test]
430 fn origin_settings_default_rejects_all() {
431 let settings = OriginValidationSettings::default();
432 let config = OriginValidationConfig::from(&settings);
433 assert!(config.reject_missing_origin);
434 assert!(matches!(config.policy, OriginPolicy::AllowList(ref v) if v.is_empty()));
435 }
436
437 #[test]
438 fn origin_settings_allow_list_roundtrips() {
439 let settings = OriginValidationSettings {
440 policy: OriginPolicySettings::AllowList(vec!["https://example.com".to_string()]),
441 reject_missing_origin: false,
442 };
443 let config = OriginValidationConfig::from(&settings);
444 assert!(!config.reject_missing_origin);
445 match config.policy {
446 OriginPolicy::AllowList(origins) => {
447 assert_eq!(origins, vec!["https://example.com".to_string()]);
448 }
449 OriginPolicy::AllowAll => panic!("expected AllowList"),
450 }
451 }
452
453 #[test]
454 fn rate_limit_settings_default_converts() {
455 let settings = RateLimitSettings::default();
456 let config = WebSocketRateLimitConfig::from(&settings);
457 assert_eq!(config.max_connections_per_window(), 20);
458 assert_eq!(config.connection_window(), Duration::from_secs(60));
459 assert_eq!(config.max_concurrent_connections_per_ip(), 10);
460 assert_eq!(config.max_messages_per_window(), 100);
461 assert_eq!(config.message_window(), Duration::from_secs(60));
462 }
463
464 #[test]
465 fn origin_policy_settings_serde_roundtrip() {
466 let settings = OriginValidationSettings::default();
467 let json = serde_json::to_string(&settings).expect("serialize");
468 let parsed: OriginValidationSettings = serde_json::from_str(&json).expect("deserialize");
469 assert_eq!(parsed.reject_missing_origin, settings.reject_missing_origin);
470 }
471
472 #[cfg(feature = "redis-channel")]
473 #[test]
474 fn redis_settings_default_converts() {
475 let settings = RedisChannelSettings::default();
476 let config = RedisConfig::from(&settings);
477 assert_eq!(config.url, "redis://127.0.0.1:6379");
478 assert_eq!(config.channel_prefix, "ws:channel:");
479 assert_eq!(config.group_prefix, "ws:group:");
480 assert_eq!(config.message_expiry, 60);
481 assert!(config.password.is_none());
482 assert!(config.username.is_none());
483 assert!(!config.tls);
484 assert!(config.require_auth);
485 }
486}