1use serde::Deserialize;
6use wsprism_core::error::{Result, WsPrismError};
7
8#[derive(Debug, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct GatewayConfig {
11 pub version: u32,
13
14 #[serde(default)]
15 pub gateway: GatewaySection,
16
17 #[serde(default)]
18 pub tenants: Vec<TenantConfig>,
19}
20
21impl GatewayConfig {
22 pub fn validate(&self) -> Result<()> {
23 if self.version != 1 {
24 return Err(WsPrismError::UnsupportedVersion);
25 }
26 if self.tenants.is_empty() {
27 return Err(WsPrismError::BadRequest("tenants must not be empty".into()));
28 }
29
30 {
32 use std::collections::HashSet;
33 let mut seen = HashSet::new();
34 for t in &self.tenants {
35 if t.id.trim().is_empty() {
36 return Err(WsPrismError::BadRequest("tenant.id must not be empty".into()));
37 }
38 if !seen.insert(t.id.clone()) {
39 return Err(WsPrismError::BadRequest(format!("duplicate tenant id: {}", t.id)));
40 }
41 t.validate()?;
42 }
43 }
44
45 self.gateway.validate()?;
46 Ok(())
47 }
48}
49
50#[derive(Debug, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct GatewaySection {
53 #[serde(default = "default_listen")]
55 pub listen: String,
56
57 #[serde(default = "default_ping_interval_ms")]
59 pub ping_interval_ms: u64,
60
61 #[serde(default = "default_idle_timeout_ms")]
64 pub idle_timeout_ms: u64,
65
66 #[serde(default = "default_writer_send_timeout_ms")]
71 pub writer_send_timeout_ms: u64,
72
73 #[serde(default = "default_drain_grace_ms")]
77 pub drain_grace_ms: u64,
78
79 #[serde(default)]
81 pub handshake_limit: HandshakeConfig,
82}
83
84#[derive(Debug, Deserialize, Clone)]
85#[serde(deny_unknown_fields)]
86pub struct HandshakeConfig {
87 #[serde(default)]
88 pub enabled: bool,
89
90 #[serde(default = "default_hs_global_burst")]
92 pub global_burst: u32,
93 #[serde(default = "default_hs_global_rps")]
95 pub global_rps: u32,
96
97 #[serde(default = "default_hs_ip_burst")]
99 pub per_ip_burst: u32,
100 #[serde(default = "default_hs_ip_rps")]
102 pub per_ip_rps: u32,
103
104 #[serde(default = "default_hs_max_entries")]
106 pub max_ip_entries: usize,
107}
108
109impl Default for HandshakeConfig {
110 fn default() -> Self {
111 Self {
112 enabled: false,
113 global_burst: 200,
114 global_rps: 100,
115 per_ip_burst: 50,
116 per_ip_rps: 10,
117 max_ip_entries: 50_000,
118 }
119 }
120}
121
122fn default_hs_global_burst() -> u32 { 200 }
124fn default_hs_global_rps() -> u32 { 100 }
125fn default_hs_ip_burst() -> u32 { 50 }
126fn default_hs_ip_rps() -> u32 { 10 }
127fn default_hs_max_entries() -> usize { 50_000 }
128
129impl Default for GatewaySection {
130 fn default() -> Self {
131 Self {
132 listen: default_listen(),
133 ping_interval_ms: default_ping_interval_ms(),
134 idle_timeout_ms: default_idle_timeout_ms(),
135 writer_send_timeout_ms: default_writer_send_timeout_ms(),
136 drain_grace_ms: default_drain_grace_ms(),
137 handshake_limit: HandshakeConfig::default(),
138 }
139 }
140}
141
142impl GatewaySection {
143 pub fn validate(&self) -> Result<()> {
144 if !(5000..=120000).contains(&self.ping_interval_ms) {
145 return Err(WsPrismError::BadRequest(
146 "gateway.ping_interval_ms must be between 5000 and 120000".into(),
147 ));
148 }
149 if !(10000..=600000).contains(&self.idle_timeout_ms) {
150 return Err(WsPrismError::BadRequest(
151 "gateway.idle_timeout_ms must be between 10000 and 600000".into(),
152 ));
153 }
154 if self.idle_timeout_ms <= self.ping_interval_ms {
155 return Err(WsPrismError::BadRequest(
156 "gateway.idle_timeout_ms must be greater than ping_interval_ms".into(),
157 ));
158 }
159 if !(50..=60000).contains(&self.writer_send_timeout_ms) {
160 return Err(WsPrismError::BadRequest(
161 "gateway.writer_send_timeout_ms must be between 50 and 60000".into(),
162 ));
163 }
164 if self.drain_grace_ms > 600000 {
165 return Err(WsPrismError::BadRequest(
166 "gateway.drain_grace_ms must be <= 600000".into(),
167 ));
168 }
169 Ok(())
170 }
171}
172
173fn default_listen() -> String { "0.0.0.0:8080".into() }
174fn default_ping_interval_ms() -> u64 { 20000 }
175fn default_idle_timeout_ms() -> u64 { 60000 }
176fn default_writer_send_timeout_ms() -> u64 { 1500 }
177fn default_drain_grace_ms() -> u64 { 2000 }
178
179#[derive(Debug, Deserialize, Clone)]
180#[serde(deny_unknown_fields)]
181pub struct TenantConfig {
182 pub id: String,
184
185 #[serde(default)]
186 pub limits: TenantLimits,
187
188 #[serde(default)]
190 pub policy: TenantPolicy,
191}
192
193impl TenantConfig {
194 pub fn validate(&self) -> Result<()> {
195 if self.limits.max_frame_bytes == 0 {
196 return Err(WsPrismError::BadRequest("limits.max_frame_bytes must be > 0".into()));
197 }
198 self.policy.validate()?;
199 Ok(())
200 }
201}
202
203#[derive(Debug, Deserialize, Clone)]
204#[serde(deny_unknown_fields)]
205pub struct TenantLimits {
206#[serde(default = "default_max_frame_bytes")]
208pub max_frame_bytes: usize,
209
210#[serde(default)]
214pub max_sessions_total: u64,
215#[serde(default)]
218pub max_rooms_total: u64,
219#[serde(default)]
221pub max_users_per_room: u64,
222#[serde(default)]
224pub max_rooms_per_user: u64,
225}
226
227impl Default for TenantLimits {
228 fn default() -> Self {
229 Self {
230 max_frame_bytes: 4096,
231 max_sessions_total: 0,
232 max_rooms_total: 0,
233 max_users_per_room: 0,
234 max_rooms_per_user: 0,
235 }
236 }
237}
238
239fn default_max_frame_bytes() -> usize { 4096 }
240
241#[derive(Debug, Deserialize, Clone, Copy)]
242#[serde(rename_all = "snake_case")]
243pub enum RateLimitScope {
244 Tenant,
245 Connection,
246 Both,
247}
248
249fn default_rate_limit_scope() -> RateLimitScope { RateLimitScope::Connection }
250
251#[derive(Debug, Deserialize, Clone, Copy)]
252#[serde(rename_all = "snake_case")]
253pub enum HotErrorMode {
254 SysError,
255 Silent,
256}
257
258fn default_hot_error_mode() -> HotErrorMode { HotErrorMode::SysError }
259
260#[derive(Debug, Deserialize, Clone, Copy)]
261#[serde(rename_all = "snake_case")]
262pub enum SessionMode {
263 Single,
264 Multi,
265}
266
267#[derive(Debug, Deserialize, Clone, Copy)]
268#[serde(rename_all = "snake_case")]
269pub enum OnExceed {
270 Deny,
271 KickOldest,
272}
273
274#[derive(Debug, Deserialize, Clone)]
275#[serde(deny_unknown_fields)]
276pub struct SessionPolicy {
277 #[serde(default = "default_session_mode")]
278 pub mode: SessionMode,
279
280 #[serde(default = "default_max_sessions_per_user")]
281 pub max_sessions_per_user: u32,
282
283 #[serde(default = "default_on_exceed")]
284 pub on_exceed: OnExceed,
285}
286
287fn default_session_mode() -> SessionMode { SessionMode::Multi }
288fn default_max_sessions_per_user() -> u32 { 4 }
289fn default_on_exceed() -> OnExceed { OnExceed::Deny }
290
291impl Default for SessionPolicy {
292 fn default() -> Self {
293 Self {
294 mode: default_session_mode(),
295 max_sessions_per_user: default_max_sessions_per_user(),
296 on_exceed: default_on_exceed(),
297 }
298 }
299}
300
301#[derive(Debug, Deserialize, Clone)]
304#[serde(deny_unknown_fields)]
305pub struct TenantPolicy {
306 #[serde(default = "default_rate_limit_rps")]
308 pub rate_limit_rps: u32,
309
310 #[serde(default = "default_rate_limit_burst")]
312 pub rate_limit_burst: u32,
313
314 #[serde(default = "default_rate_limit_scope")]
316 pub rate_limit_scope: RateLimitScope,
317
318 #[serde(default = "default_ext_allowlist")]
320 pub ext_allowlist: Vec<String>,
321
322 #[serde(default)]
324 pub hot_allowlist: Vec<String>,
325
326 #[serde(default)]
328 pub sessions: SessionPolicy,
329
330 #[serde(default = "default_hot_error_mode")]
332 pub hot_error_mode: HotErrorMode,
333
334 #[serde(default = "default_hot_requires_active_room")]
336 pub hot_requires_active_room: bool,
337}
338
339fn default_hot_requires_active_room() -> bool { true }
340
341impl Default for TenantPolicy {
342 fn default() -> Self {
343 Self {
344 rate_limit_rps: default_rate_limit_rps(),
345 rate_limit_burst: default_rate_limit_burst(),
346 rate_limit_scope: default_rate_limit_scope(),
347 ext_allowlist: default_ext_allowlist(),
348 hot_allowlist: Vec::new(),
349 sessions: SessionPolicy::default(),
350 hot_error_mode: default_hot_error_mode(),
351 hot_requires_active_room: default_hot_requires_active_room(),
352 }
353 }
354}
355
356impl TenantPolicy {
357 pub fn validate(&self) -> Result<()> {
358 if self.rate_limit_rps == 0 || self.rate_limit_burst == 0 {
359 return Err(WsPrismError::BadRequest(
360 "policy.rate_limit_rps and rate_limit_burst must be > 0".into(),
361 ));
362 }
363 match self.sessions.mode {
365 SessionMode::Single => {
366 if self.sessions.max_sessions_per_user != 1 {
367 return Err(WsPrismError::BadRequest(
368 "policy.sessions.mode=single requires max_sessions_per_user=1".into(),
369 ));
370 }
371 }
372 SessionMode::Multi => {
373 if self.sessions.max_sessions_per_user == 0 {
374 return Err(WsPrismError::BadRequest(
375 "policy.sessions.max_sessions_per_user must be > 0".into(),
376 ));
377 }
378 }
379 }
380 Ok(())
381 }
382}
383
384fn default_rate_limit_rps() -> u32 { 200 }
385fn default_rate_limit_burst() -> u32 { 400 }
386fn default_ext_allowlist() -> Vec<String> {
387 vec!["room:join".into(), "room:leave".into()]
388}