1use crate::account_policy::{redact_identity, AccountPolicyError, AccountPolicyRegistry};
2use crate::compression::CompressedPayload;
3use crate::websocket::auth::{AuthContext, AuthDeny, AuthErrorCode};
4use crate::websocket::rate_limiter::{RateLimitResult, WebSocketRateLimiter};
5use arete_auth::Limits;
6use bytes::Bytes;
7use dashmap::DashMap;
8use futures_util::stream::SplitSink;
9use futures_util::SinkExt;
10use std::collections::{HashMap, HashSet};
11use std::net::SocketAddr;
12use std::sync::Arc;
13use std::time::{Duration, Instant, SystemTime};
14use tokio::net::TcpStream;
15use tokio::sync::{mpsc, RwLock};
16use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
17use tokio_tungstenite::tungstenite::protocol::CloseFrame;
18use tokio_tungstenite::{tungstenite::Message, WebSocketStream};
19use tokio_util::sync::CancellationToken;
20use tracing::{debug, info, warn};
21use uuid::Uuid;
22
23pub type WebSocketSender = SplitSink<WebSocketStream<TcpStream>, Message>;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum SendError {
28 ClientNotFound,
30 ClientBackpressured,
32 ClientDisconnected,
34}
35
36impl std::fmt::Display for SendError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 SendError::ClientNotFound => write!(f, "client not found"),
40 SendError::ClientBackpressured => write!(f, "client backpressured and disconnected"),
41 SendError::ClientDisconnected => write!(f, "client disconnected"),
42 }
43 }
44}
45
46impl std::error::Error for SendError {}
47
48#[derive(Debug)]
50struct EgressTracker {
51 bytes_this_minute: u64,
53 window_start: SystemTime,
55}
56
57#[derive(Debug)]
59struct MessageRateTracker {
60 messages_this_minute: u32,
61 window_start: SystemTime,
62}
63
64impl MessageRateTracker {
65 fn new() -> Self {
66 Self {
67 messages_this_minute: 0,
68 window_start: SystemTime::now(),
69 }
70 }
71
72 fn maybe_reset_window(&mut self) {
73 let now = SystemTime::now();
74 if now.duration_since(self.window_start).unwrap_or_default() >= Duration::from_secs(60) {
75 self.messages_this_minute = 0;
76 self.window_start = now;
77 }
78 }
79
80 fn record_message(&mut self, limit: u32) -> bool {
81 self.maybe_reset_window();
82 if self.messages_this_minute + 1 > limit {
83 false
84 } else {
85 self.messages_this_minute += 1;
86 true
87 }
88 }
89
90 fn current_usage(&mut self) -> u32 {
91 self.maybe_reset_window();
92 self.messages_this_minute
93 }
94}
95
96impl EgressTracker {
97 fn new() -> Self {
98 Self {
99 bytes_this_minute: 0,
100 window_start: SystemTime::now(),
101 }
102 }
103
104 fn maybe_reset_window(&mut self) {
106 let now = SystemTime::now();
107 if now.duration_since(self.window_start).unwrap_or_default() >= Duration::from_secs(60) {
108 self.bytes_this_minute = 0;
109 self.window_start = now;
110 }
111 }
112
113 fn record_bytes(&mut self, bytes: usize, limit: u64) -> bool {
115 self.maybe_reset_window();
116 let bytes_u64 = bytes as u64;
117 if self.bytes_this_minute + bytes_u64 > limit {
118 false
119 } else {
120 self.bytes_this_minute += bytes_u64;
121 true
122 }
123 }
124
125 fn current_usage(&mut self) -> u64 {
127 self.maybe_reset_window();
128 self.bytes_this_minute
129 }
130}
131
132#[derive(Debug)]
137struct AccountUsage {
138 egress: std::sync::Mutex<EgressTracker>,
139 messages: std::sync::Mutex<MessageRateTracker>,
140 last_seen: std::sync::Mutex<Instant>,
141}
142
143impl AccountUsage {
144 fn new() -> Self {
145 Self {
146 egress: std::sync::Mutex::new(EgressTracker::new()),
147 messages: std::sync::Mutex::new(MessageRateTracker::new()),
148 last_seen: std::sync::Mutex::new(Instant::now()),
149 }
150 }
151
152 fn touch(&self) {
153 if let Ok(mut last_seen) = self.last_seen.lock() {
154 *last_seen = Instant::now();
155 }
156 }
157
158 fn record_egress(&self, bytes: usize, limit: u64) -> bool {
159 self.touch();
160 match self.egress.lock() {
161 Ok(mut tracker) => tracker.record_bytes(bytes, limit),
162 Err(_) => true,
163 }
164 }
165
166 fn record_message(&self, limit: u32) -> bool {
167 self.touch();
168 match self.messages.lock() {
169 Ok(mut tracker) => tracker.record_message(limit),
170 Err(_) => true,
171 }
172 }
173
174 fn idle_for(&self) -> Duration {
175 self.last_seen
176 .lock()
177 .map(|last_seen| last_seen.elapsed())
178 .unwrap_or(Duration::MAX)
179 }
180}
181
182#[derive(Debug)]
184pub struct ClientInfo {
185 pub id: Uuid,
186 pub last_seen: SystemTime,
187 pub sender: mpsc::Sender<Message>,
188 subscriptions: Arc<RwLock<HashMap<String, CancellationToken>>>,
189 pub auth_context: Option<AuthContext>,
191 pub remote_addr: SocketAddr,
193 egress_tracker: std::sync::Mutex<EgressTracker>,
195 message_rate_tracker: std::sync::Mutex<MessageRateTracker>,
197 close_frame: Arc<std::sync::OnceLock<CloseFrame>>,
201}
202
203impl ClientInfo {
204 pub fn new(
205 id: Uuid,
206 sender: mpsc::Sender<Message>,
207 auth_context: Option<AuthContext>,
208 remote_addr: SocketAddr,
209 ) -> Self {
210 Self {
211 id,
212 last_seen: SystemTime::now(),
213 sender,
214 subscriptions: Arc::new(RwLock::new(HashMap::new())),
215 auth_context,
216 remote_addr,
217 egress_tracker: std::sync::Mutex::new(EgressTracker::new()),
218 message_rate_tracker: std::sync::Mutex::new(MessageRateTracker::new()),
219 close_frame: Arc::new(std::sync::OnceLock::new()),
220 }
221 }
222
223 pub fn record_egress(&self, bytes: usize) -> Option<u64> {
225 if let Ok(mut tracker) = self.egress_tracker.lock() {
226 if let Some(ref ctx) = self.auth_context {
227 if let Some(limit) = ctx.limits.max_bytes_per_minute {
228 if tracker.record_bytes(bytes, limit) {
229 return Some(tracker.current_usage());
230 } else {
231 return None; }
233 }
234 }
235 return Some(tracker.current_usage());
237 }
238 None
239 }
240
241 pub fn record_inbound_message(&self) -> Option<u32> {
243 if let Ok(mut tracker) = self.message_rate_tracker.lock() {
244 if let Some(ref ctx) = self.auth_context {
245 if let Some(limit) = ctx.limits.max_messages_per_minute {
246 if tracker.record_message(limit) {
247 return Some(tracker.current_usage());
248 } else {
249 return None;
250 }
251 }
252 }
253
254 return Some(tracker.current_usage());
255 }
256
257 None
258 }
259
260 pub fn update_last_seen(&mut self) {
261 self.last_seen = SystemTime::now();
262 }
263
264 pub fn is_stale(&self, timeout: Duration) -> bool {
265 self.last_seen.elapsed().unwrap_or(Duration::MAX) > timeout
266 }
267
268 pub async fn add_subscription(
269 &self,
270 subscription_id: String,
271 token: CancellationToken,
272 ) -> bool {
273 let mut subs = self.subscriptions.write().await;
274 match subs.entry(subscription_id) {
275 std::collections::hash_map::Entry::Vacant(entry) => {
276 entry.insert(token);
277 true
278 }
279 std::collections::hash_map::Entry::Occupied(_) => false,
280 }
281 }
282
283 pub async fn remove_subscription(&self, subscription_id: &str) -> bool {
284 let mut subs = self.subscriptions.write().await;
285 if let Some(token) = subs.remove(subscription_id) {
286 token.cancel();
287 debug!("Cancelled subscription: {}", subscription_id);
288 true
289 } else {
290 debug!(
291 "Subscription not found for cancellation: {}",
292 subscription_id
293 );
294 false
295 }
296 }
297
298 pub async fn cancel_all_subscriptions(&self) {
299 let subs = self.subscriptions.read().await;
300 for (subscription_id, token) in subs.iter() {
301 token.cancel();
302 debug!("Cancelled subscription on disconnect: {}", subscription_id);
303 }
304 }
305
306 pub async fn subscription_count(&self) -> usize {
307 self.subscriptions.read().await.len()
308 }
309}
310
311#[derive(Debug, Clone)]
316pub struct RateLimitConfig {
317 pub max_connections_per_ip: Option<usize>,
319 pub max_connections_per_metering_key: Option<usize>,
321 pub max_connections_per_origin: Option<usize>,
323 pub client_timeout: Duration,
325 pub message_queue_size: usize,
327 pub max_reconnect_attempts: Option<u32>,
329 pub message_rate_window: Duration,
331 pub egress_rate_window: Duration,
333 pub default_limits: Option<Limits>,
336}
337
338impl Default for RateLimitConfig {
339 fn default() -> Self {
340 Self {
341 max_connections_per_ip: None,
342 max_connections_per_metering_key: None,
343 max_connections_per_origin: None,
344 client_timeout: Duration::from_secs(300),
345 message_queue_size: 512,
346 max_reconnect_attempts: None,
347 message_rate_window: Duration::from_secs(60),
348 egress_rate_window: Duration::from_secs(60),
349 default_limits: None,
350 }
351 }
352}
353
354impl RateLimitConfig {
355 pub fn from_env() -> Self {
370 let mut config = Self::default();
371
372 if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_IP") {
373 if let Ok(max) = val.parse() {
374 config.max_connections_per_ip = Some(max);
375 }
376 }
377
378 if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_METERING_KEY") {
379 if let Ok(max) = val.parse() {
380 config.max_connections_per_metering_key = Some(max);
381 }
382 }
383
384 if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_ORIGIN") {
385 if let Ok(max) = val.parse() {
386 config.max_connections_per_origin = Some(max);
387 }
388 }
389
390 if let Ok(val) = std::env::var("ARETE_WS_CLIENT_TIMEOUT_SECS") {
391 if let Ok(secs) = val.parse() {
392 config.client_timeout = Duration::from_secs(secs);
393 }
394 }
395
396 if let Ok(val) = std::env::var("ARETE_WS_MESSAGE_QUEUE_SIZE") {
397 if let Ok(size) = val.parse() {
398 config.message_queue_size = size;
399 }
400 }
401
402 if let Ok(val) = std::env::var("ARETE_WS_RATE_LIMIT_WINDOW_SECS") {
403 if let Ok(secs) = val.parse() {
404 config.message_rate_window = Duration::from_secs(secs);
405 config.egress_rate_window = Duration::from_secs(secs);
406 }
407 }
408
409 let mut default_limits = Limits::default();
411 let mut has_default_limits = false;
412
413 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_CONNECTIONS") {
414 if let Ok(max) = val.parse() {
415 default_limits.max_connections = Some(max);
416 has_default_limits = true;
417 }
418 }
419
420 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SUBSCRIPTIONS") {
421 if let Ok(max) = val.parse() {
422 default_limits.max_subscriptions = Some(max);
423 has_default_limits = true;
424 }
425 }
426
427 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SNAPSHOT_ROWS") {
428 if let Ok(max) = val.parse() {
429 default_limits.max_snapshot_rows = Some(max);
430 has_default_limits = true;
431 }
432 }
433
434 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_MESSAGES_PER_MINUTE") {
435 if let Ok(max) = val.parse() {
436 default_limits.max_messages_per_minute = Some(max);
437 has_default_limits = true;
438 }
439 }
440
441 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_BYTES_PER_MINUTE") {
442 if let Ok(max) = val.parse() {
443 default_limits.max_bytes_per_minute = Some(max);
444 has_default_limits = true;
445 }
446 }
447
448 if has_default_limits {
449 config.default_limits = Some(default_limits);
450 }
451
452 config
453 }
454
455 pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
457 self.max_connections_per_ip = Some(max);
458 self
459 }
460
461 pub fn with_timeout(mut self, timeout: Duration) -> Self {
463 self.client_timeout = timeout;
464 self
465 }
466
467 pub fn with_message_queue_size(mut self, size: usize) -> Self {
469 self.message_queue_size = size;
470 self
471 }
472
473 pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
475 self.message_rate_window = window;
476 self.egress_rate_window = window;
477 self
478 }
479
480 pub fn with_default_limits(mut self, limits: Limits) -> Self {
485 self.default_limits = Some(limits);
486 self
487 }
488}
489
490const MAX_TRACKED_ACCOUNT_USAGE: usize = crate::account_policy::DEFAULT_MAX_TRACKED_ACCOUNTS;
493
494const ACCOUNT_USAGE_IDLE_TTL: Duration = crate::account_policy::DEFAULT_ACCOUNT_IDLE_TTL;
496
497#[derive(Clone)]
506pub struct ClientManager {
507 clients: Arc<DashMap<Uuid, ClientInfo>>,
508 rate_limit_config: RateLimitConfig,
509 rate_limiter: Option<Arc<WebSocketRateLimiter>>,
511 account_policies: Arc<AccountPolicyRegistry>,
513 account_usage: Arc<DashMap<String, AccountUsage>>,
515}
516
517impl ClientManager {
518 pub fn new() -> Self {
519 Self::with_config(RateLimitConfig::default())
520 }
521
522 pub fn with_config(config: RateLimitConfig) -> Self {
524 Self {
525 clients: Arc::new(DashMap::new()),
526 rate_limit_config: config,
527 rate_limiter: None,
528 account_policies: Arc::new(AccountPolicyRegistry::default()),
529 account_usage: Arc::new(DashMap::new()),
530 }
531 }
532
533 pub fn from_env() -> Self {
537 Self::with_config(RateLimitConfig::from_env())
538 }
539
540 pub fn with_timeout(mut self, timeout: Duration) -> Self {
542 self.rate_limit_config.client_timeout = timeout;
543 self
544 }
545
546 pub fn with_message_queue_size(mut self, queue_size: usize) -> Self {
548 self.rate_limit_config.message_queue_size = queue_size;
549 self
550 }
551
552 pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
554 self.rate_limit_config.max_connections_per_ip = Some(max);
555 self
556 }
557
558 pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
560 self.rate_limit_config.message_rate_window = window;
561 self.rate_limit_config.egress_rate_window = window;
562 self
563 }
564
565 pub fn with_default_limits(mut self, limits: Limits) -> Self {
570 self.rate_limit_config.default_limits = Some(limits);
571 self
572 }
573
574 pub fn with_rate_limiter(mut self, rate_limiter: Arc<WebSocketRateLimiter>) -> Self {
576 self.rate_limiter = Some(rate_limiter);
577 self
578 }
579
580 pub fn rate_limiter(&self) -> Option<&WebSocketRateLimiter> {
582 self.rate_limiter.as_ref().map(|r| r.as_ref())
583 }
584
585 pub fn rate_limit_config(&self) -> &RateLimitConfig {
587 &self.rate_limit_config
588 }
589
590 pub fn account_policies(&self) -> &AccountPolicyRegistry {
592 &self.account_policies
593 }
594
595 fn record_account_egress(&self, ctx: &AuthContext, bytes: usize) -> bool {
597 if ctx.is_legacy_policy() || ctx.account_key.is_none() {
598 return true;
599 }
600 let Some(limit) = ctx.account_limits.max_bytes_per_minute else {
601 return true;
602 };
603 let account = ctx.account_key();
604 if let Some(usage) = self.account_usage.get(account) {
605 return usage.record_egress(bytes, limit);
606 }
607 if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
608 warn!(
609 account = %redact_identity(account),
610 "account usage tracking at capacity; aggregate egress guard skipped"
611 );
612 return true;
613 }
614 self.account_usage
615 .entry(account.to_string())
616 .or_insert_with(AccountUsage::new)
617 .record_egress(bytes, limit)
618 }
619
620 fn record_account_message(&self, ctx: &AuthContext) -> bool {
622 if ctx.is_legacy_policy() || ctx.account_key.is_none() {
623 return true;
624 }
625 let Some(limit) = ctx.account_limits.max_messages_per_minute else {
626 return true;
627 };
628 let account = ctx.account_key();
629 if let Some(usage) = self.account_usage.get(account) {
630 return usage.record_message(limit);
631 }
632 if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
633 warn!(
634 account = %redact_identity(account),
635 "account usage tracking at capacity; aggregate message guard skipped"
636 );
637 return true;
638 }
639 self.account_usage
640 .entry(account.to_string())
641 .or_insert_with(AccountUsage::new)
642 .record_message(limit)
643 }
644
645 fn enforce_egress_budgets(&self, client_id: Uuid, bytes: usize) -> Result<(), SendError> {
647 let (connection_ok, account_ok) = {
648 let Some(client) = self.clients.get(&client_id) else {
649 return Err(SendError::ClientNotFound);
650 };
651 let connection_ok = client.record_egress(bytes).is_some();
652 let account_ok = !connection_ok
653 || client
654 .auth_context
655 .as_ref()
656 .map(|ctx| self.record_account_egress(ctx, bytes))
657 .unwrap_or(true);
658 (connection_ok, account_ok)
659 };
660 if !connection_ok {
661 warn!("Client {} exceeded egress limit, disconnecting", client_id);
662 self.clients.remove(&client_id);
663 return Err(SendError::ClientDisconnected);
664 }
665 if !account_ok {
666 warn!(
667 "Client {} exceeded the account egress limit, disconnecting",
668 client_id
669 );
670 self.clients.remove(&client_id);
671 return Err(SendError::ClientDisconnected);
672 }
673 Ok(())
674 }
675
676 pub fn add_client(
682 &self,
683 client_id: Uuid,
684 mut ws_sender: WebSocketSender,
685 auth_context: Option<AuthContext>,
686 remote_addr: SocketAddr,
687 ) {
688 let (client_tx, mut client_rx) =
689 mpsc::channel::<Message>(self.rate_limit_config.message_queue_size);
690 let client_info = ClientInfo::new(client_id, client_tx, auth_context, remote_addr);
691 let close_frame = client_info.close_frame.clone();
692
693 let clients_ref = self.clients.clone();
694 tokio::spawn(async move {
695 while let Some(message) = client_rx.recv().await {
696 if let Err(e) = ws_sender.send(message).await {
697 warn!("Failed to send message to client {}: {}", client_id, e);
698 break;
699 }
700 }
701 if let Some(frame) = close_frame.get() {
706 let _ = ws_sender.send(Message::Close(Some(frame.clone()))).await;
707 }
708 let _ = ws_sender.close().await;
709 clients_ref.remove(&client_id);
710 debug!("WebSocket sender task for client {} stopped", client_id);
711 });
712
713 self.clients.insert(client_id, client_info);
714 info!("Client {} registered from {}", client_id, remote_addr);
715 }
716
717 pub fn remove_client(&self, client_id: Uuid) {
719 if self.clients.remove(&client_id).is_some() {
720 info!("Client {} removed", client_id);
721 }
722 }
723
724 pub fn update_client_auth(&self, client_id: Uuid, auth_context: AuthContext) -> bool {
728 if let Some(mut client) = self.clients.get_mut(&client_id) {
729 client.auth_context = Some(auth_context);
730 debug!("Updated auth context for client {}", client_id);
731 true
732 } else {
733 false
734 }
735 }
736
737 pub fn check_and_remove_expired(&self, client_id: Uuid) -> bool {
742 let now = std::time::SystemTime::now()
743 .duration_since(std::time::UNIX_EPOCH)
744 .unwrap_or_default()
745 .as_secs();
746 let Some((_, client)) = self.clients.remove_if(&client_id, |_, client| {
750 client
751 .auth_context
752 .as_ref()
753 .is_some_and(|ctx| ctx.expires_at <= now)
754 }) else {
755 return false;
756 };
757 if let Some(ctx) = &client.auth_context {
758 warn!(
759 "Client {} token expired (expired at {}), disconnecting",
760 client_id, ctx.expires_at
761 );
762 }
763 let _ = client.close_frame.set(CloseFrame {
767 code: CloseCode::Policy,
768 reason: format!(
769 "{}: Authentication token expired",
770 AuthErrorCode::TokenExpired.as_str()
771 )
772 .into(),
773 });
774 true
775 }
776
777 pub fn client_count(&self) -> usize {
782 self.clients.len()
783 }
784
785 pub fn send_to_client(&self, client_id: Uuid, data: Arc<Bytes>) -> Result<(), SendError> {
794 if self.check_and_remove_expired(client_id) {
796 return Err(SendError::ClientDisconnected);
797 }
798
799 self.enforce_egress_budgets(client_id, data.len())?;
801
802 let sender = {
803 let client = self
804 .clients
805 .get(&client_id)
806 .ok_or(SendError::ClientNotFound)?;
807 client.sender.clone()
808 };
809
810 let msg = Message::Binary((*data).clone());
811 match sender.try_send(msg) {
812 Ok(()) => Ok(()),
813 Err(mpsc::error::TrySendError::Full(_)) => {
814 warn!(
815 "Client {} backpressured (queue full), disconnecting",
816 client_id
817 );
818 self.clients.remove(&client_id);
819 Err(SendError::ClientBackpressured)
820 }
821 Err(mpsc::error::TrySendError::Closed(_)) => {
822 debug!("Client {} channel closed", client_id);
823 self.clients.remove(&client_id);
824 Err(SendError::ClientDisconnected)
825 }
826 }
827 }
828
829 pub async fn send_to_client_async(
838 &self,
839 client_id: Uuid,
840 data: Arc<Bytes>,
841 ) -> Result<(), SendError> {
842 if self.check_and_remove_expired(client_id) {
844 return Err(SendError::ClientDisconnected);
845 }
846
847 self.enforce_egress_budgets(client_id, data.len())?;
849
850 let sender = {
851 let client = self
852 .clients
853 .get(&client_id)
854 .ok_or(SendError::ClientNotFound)?;
855 client.sender.clone()
856 };
857
858 let msg = Message::Binary((*data).clone());
859 sender
860 .send(msg)
861 .await
862 .map_err(|_| SendError::ClientDisconnected)
863 }
864
865 pub async fn send_text_to_client(
870 &self,
871 client_id: Uuid,
872 text: String,
873 ) -> Result<(), SendError> {
874 if self.check_and_remove_expired(client_id) {
876 return Err(SendError::ClientDisconnected);
877 }
878
879 let sender = {
880 let client = self
881 .clients
882 .get(&client_id)
883 .ok_or(SendError::ClientNotFound)?;
884 client.sender.clone()
885 };
886
887 let msg = Message::Text(text.into());
888 sender
889 .send(msg)
890 .await
891 .map_err(|_| SendError::ClientDisconnected)
892 }
893
894 pub async fn send_compressed_async(
899 &self,
900 client_id: Uuid,
901 payload: CompressedPayload,
902 ) -> Result<(), SendError> {
903 if self.check_and_remove_expired(client_id) {
905 return Err(SendError::ClientDisconnected);
906 }
907
908 let (sender, bytes_to_record) = {
909 let client = self
910 .clients
911 .get(&client_id)
912 .ok_or(SendError::ClientNotFound)?;
913
914 let bytes = match &payload {
915 CompressedPayload::Compressed(bytes) => bytes.len(),
916 CompressedPayload::Uncompressed(bytes) => bytes.len(),
917 };
918
919 (client.sender.clone(), bytes)
920 };
921
922 self.enforce_egress_budgets(client_id, bytes_to_record)?;
924
925 let msg = match payload {
926 CompressedPayload::Compressed(bytes) => Message::Binary(bytes),
927 CompressedPayload::Uncompressed(bytes) => Message::Binary(bytes),
928 };
929 sender
930 .send(msg)
931 .await
932 .map_err(|_| SendError::ClientDisconnected)
933 }
934
935 pub fn update_client_last_seen(&self, client_id: Uuid) {
937 if let Some(mut client) = self.clients.get_mut(&client_id) {
938 client.update_last_seen();
939 }
940 }
941
942 #[allow(clippy::result_large_err)]
944 pub fn check_inbound_message_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
945 if self.check_and_remove_expired(client_id) {
946 return Err(AuthDeny::new(
947 crate::websocket::auth::AuthErrorCode::TokenExpired,
948 "Authentication token expired",
949 ));
950 }
951
952 let (connection_ok, account_ok) = {
953 let Some(client) = self.clients.get(&client_id) else {
954 return Err(AuthDeny::new(
955 crate::websocket::auth::AuthErrorCode::InternalError,
956 "Client not found",
957 ));
958 };
959 let connection_ok = client.record_inbound_message().is_some();
960 let account_ok = !connection_ok
961 || client
962 .auth_context
963 .as_ref()
964 .map(|ctx| self.record_account_message(ctx))
965 .unwrap_or(true);
966 (connection_ok, account_ok)
967 };
968
969 if connection_ok && account_ok {
970 return Ok(());
971 }
972 self.clients.remove(&client_id);
973 let scope = if connection_ok {
974 "inbound account websocket messages"
975 } else {
976 "inbound websocket messages"
977 };
978 Err(
979 AuthDeny::rate_limited(self.rate_limit_config.message_rate_window, scope).with_context(
980 format!("client {} exceeded the inbound message budget", client_id),
981 ),
982 )
983 }
984
985 pub fn has_client(&self, client_id: Uuid) -> bool {
987 self.clients.contains_key(&client_id)
988 }
989
990 pub async fn add_client_subscription(
991 &self,
992 client_id: Uuid,
993 subscription_id: String,
994 token: CancellationToken,
995 ) -> bool {
996 if let Some(client) = self.clients.get(&client_id) {
997 client.add_subscription(subscription_id, token).await
998 } else {
999 false
1000 }
1001 }
1002
1003 pub async fn remove_client_subscription(&self, client_id: Uuid, subscription_id: &str) -> bool {
1004 if let Some(client) = self.clients.get(&client_id) {
1005 client.remove_subscription(subscription_id).await
1006 } else {
1007 false
1008 }
1009 }
1010
1011 pub async fn cancel_all_client_subscriptions(&self, client_id: Uuid) {
1012 if let Some(client) = self.clients.get(&client_id) {
1013 client.cancel_all_subscriptions().await;
1014 }
1015 }
1016
1017 pub fn cleanup_stale_clients(&self) -> usize {
1019 let timeout = self.rate_limit_config.client_timeout;
1020 let mut stale_clients = Vec::new();
1021
1022 for entry in self.clients.iter() {
1023 if entry.value().is_stale(timeout) {
1024 stale_clients.push(*entry.key());
1025 }
1026 }
1027
1028 let removed_count = stale_clients.len();
1029 for client_id in stale_clients {
1030 self.clients.remove(&client_id);
1031 info!("Removed stale client {}", client_id);
1032 }
1033
1034 removed_count
1035 }
1036
1037 fn cleanup_account_state(&self) {
1040 let live_accounts: HashSet<String> = self
1041 .clients
1042 .iter()
1043 .filter_map(|entry| {
1044 entry
1045 .value()
1046 .auth_context
1047 .as_ref()
1048 .and_then(|ctx| ctx.account_key.clone())
1049 })
1050 .collect();
1051 self.account_usage.retain(|account, usage| {
1052 live_accounts.contains(account) || usage.idle_for() < ACCOUNT_USAGE_IDLE_TTL
1053 });
1054 self.account_policies
1055 .evict_idle(|account| live_accounts.contains(account));
1056 }
1057
1058 pub fn start_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
1063 let client_manager = self.clone();
1064
1065 tokio::spawn(async move {
1066 let mut interval = tokio::time::interval(Duration::from_secs(30));
1067
1068 loop {
1069 interval.tick().await;
1070 let removed = client_manager.cleanup_stale_clients();
1071 if removed > 0 {
1072 info!("Cleaned up {} stale clients", removed);
1073 }
1074 client_manager.cleanup_account_state();
1075 }
1076 })
1077 }
1078
1079 #[allow(clippy::result_large_err)]
1087 pub async fn check_connection_allowed(
1088 &self,
1089 remote_addr: SocketAddr,
1090 auth_context: &Option<AuthContext>,
1091 ) -> Result<(), AuthDeny> {
1092 if let Some(ref rate_limiter) = self.rate_limiter {
1094 match rate_limiter.check_handshake(remote_addr).await {
1096 RateLimitResult::Allowed { .. } => {}
1097 RateLimitResult::Denied { retry_after, limit } => {
1098 return Err(AuthDeny::rate_limited(retry_after, "websocket handshakes")
1099 .with_context(format!(
1100 "handshake rate limit of {} per minute exceeded for {}",
1101 limit, remote_addr
1102 )));
1103 }
1104 }
1105
1106 if let Some(ref ctx) = auth_context {
1107 match rate_limiter
1111 .check_connection_for_consumer(
1112 ctx.consumer_key(),
1113 ctx.limits.max_connection_attempts_per_minute,
1114 )
1115 .await
1116 {
1117 RateLimitResult::Allowed { .. } => {}
1118 RateLimitResult::Denied { retry_after, limit } => {
1119 return Err(AuthDeny::rate_limited(retry_after, "websocket connections")
1120 .with_context(format!(
1121 "connection rate limit for consumer {} of {} per minute exceeded",
1122 ctx.consumer_key(),
1123 limit
1124 )));
1125 }
1126 }
1127
1128 if ctx.is_legacy_policy() || ctx.account_key.is_some() {
1132 match rate_limiter
1133 .check_connection_for_account(
1134 ctx.account_key(),
1135 ctx.account_limits.max_connection_attempts_per_minute,
1136 )
1137 .await
1138 {
1139 RateLimitResult::Allowed { .. } => {}
1140 RateLimitResult::Denied { retry_after, limit } => {
1141 return Err(AuthDeny::rate_limited(
1142 retry_after,
1143 "metered websocket connections",
1144 )
1145 .with_context(format!(
1146 "connection rate limit for account {} of {} per minute exceeded",
1147 ctx.account_key(),
1148 limit
1149 )));
1150 }
1151 }
1152 }
1153 }
1154 }
1155
1156 if let Some(max_per_ip) = self.rate_limit_config.max_connections_per_ip {
1158 let current_ip_connections = self.count_connections_for_ip(&remote_addr);
1159 if current_ip_connections >= max_per_ip {
1160 return Err(AuthDeny::connection_limit_exceeded(
1161 &format!("ip {}", remote_addr.ip()),
1162 current_ip_connections,
1163 max_per_ip,
1164 ));
1165 }
1166 }
1167
1168 if let Some(ctx) = auth_context {
1169 if ctx.is_legacy_policy() {
1172 let legacy_policy_token = self.account_policies.record_legacy_token();
1173 debug!(legacy_policy_token, "legacy policy token admitted");
1174 } else if let (Some(account), Some(policy_version)) =
1175 (ctx.account_key.as_deref(), ctx.policy_version)
1176 {
1177 match self
1178 .account_policies
1179 .observe(account, policy_version, &ctx.account_limits)
1180 {
1181 Ok(()) => {}
1182 Err(AccountPolicyError::StaleVersion { presented, current }) => {
1183 debug!(
1184 account = %redact_identity(account),
1185 presented,
1186 current,
1187 "stale policy version rejected"
1188 );
1189 return Err(AuthDeny::new(
1190 crate::websocket::auth::AuthErrorCode::TokenExpired,
1191 "session policy version is stale; refresh the session token",
1192 ));
1193 }
1194 Err(AccountPolicyError::ConflictingLimits { version }) => {
1195 warn!(
1196 account = %redact_identity(account),
1197 version,
1198 "signed account limits conflict for one policy version"
1199 );
1200 return Err(AuthDeny::new(
1201 crate::websocket::auth::AuthErrorCode::InternalError,
1202 "signed account limits conflict with previously observed policy",
1203 ));
1204 }
1205 Err(AccountPolicyError::CapacityExhausted) => {
1206 warn!("account policy state at capacity; denying admission");
1207 return Err(AuthDeny::new(
1208 crate::websocket::auth::AuthErrorCode::InternalError,
1209 "account policy state is at capacity; retry shortly",
1210 ));
1211 }
1212 }
1213 }
1214
1215 let max_connections = ctx.limits.max_connections.or_else(|| {
1219 self.rate_limit_config
1220 .default_limits
1221 .as_ref()
1222 .and_then(|l| l.max_connections)
1223 });
1224 if let Some(max_connections) = max_connections {
1225 let current_connections = self.count_connections_for_consumer(ctx.consumer_key());
1226 if current_connections >= max_connections as usize {
1227 return Err(AuthDeny::connection_limit_exceeded(
1228 &format!("consumer {}", ctx.consumer_key()),
1229 current_connections,
1230 max_connections as usize,
1231 ));
1232 }
1233 }
1234
1235 if !ctx.is_legacy_policy() && ctx.account_key.is_some() {
1238 if let Some(max_account_connections) = ctx.account_limits.max_connections {
1239 let current_account_connections =
1240 self.count_connections_for_account(ctx.account_key());
1241 if current_account_connections >= max_account_connections as usize {
1242 return Err(AuthDeny::connection_limit_exceeded(
1243 &format!("account {}", ctx.account_key()),
1244 current_account_connections,
1245 max_account_connections as usize,
1246 ));
1247 }
1248 }
1249 }
1250
1251 if let Some(max_per_metering_key) =
1253 self.rate_limit_config.max_connections_per_metering_key
1254 {
1255 let current_metering_connections =
1256 self.count_connections_for_metering_key(&ctx.metering_key);
1257 if current_metering_connections >= max_per_metering_key {
1258 return Err(AuthDeny::connection_limit_exceeded(
1259 &format!("metering key {}", ctx.metering_key),
1260 current_metering_connections,
1261 max_per_metering_key,
1262 ));
1263 }
1264 }
1265
1266 if let Some(max_per_origin) = self.rate_limit_config.max_connections_per_origin {
1268 if let Some(ref origin) = ctx.origin {
1269 let current_origin_connections = self.count_connections_for_origin(origin);
1270 if current_origin_connections >= max_per_origin {
1271 return Err(AuthDeny::connection_limit_exceeded(
1272 &format!("origin {}", origin),
1273 current_origin_connections,
1274 max_per_origin,
1275 ));
1276 }
1277 }
1278 }
1279 }
1280 Ok(())
1281 }
1282
1283 fn count_connections_for_ip(&self, remote_addr: &SocketAddr) -> usize {
1285 let ip = remote_addr.ip();
1286 self.clients
1287 .iter()
1288 .filter(|entry| entry.value().remote_addr.ip() == ip)
1289 .count()
1290 }
1291
1292 fn count_connections_for_consumer(&self, consumer: &str) -> usize {
1294 self.clients
1295 .iter()
1296 .filter(|entry| {
1297 entry
1298 .value()
1299 .auth_context
1300 .as_ref()
1301 .map(|ctx| ctx.consumer_key() == consumer)
1302 .unwrap_or(false)
1303 })
1304 .count()
1305 }
1306
1307 fn count_connections_for_account(&self, account: &str) -> usize {
1309 self.clients
1310 .iter()
1311 .filter(|entry| {
1312 entry
1313 .value()
1314 .auth_context
1315 .as_ref()
1316 .map(|ctx| ctx.account_key() == account)
1317 .unwrap_or(false)
1318 })
1319 .count()
1320 }
1321
1322 fn count_connections_for_metering_key(&self, metering_key: &str) -> usize {
1324 self.clients
1325 .iter()
1326 .filter(|entry| {
1327 entry
1328 .value()
1329 .auth_context
1330 .as_ref()
1331 .map(|ctx| ctx.metering_key == metering_key)
1332 .unwrap_or(false)
1333 })
1334 .count()
1335 }
1336
1337 fn count_connections_for_origin(&self, origin: &str) -> usize {
1339 self.clients
1340 .iter()
1341 .filter(|entry| {
1342 entry
1343 .value()
1344 .auth_context
1345 .as_ref()
1346 .and_then(|ctx| ctx.origin.as_ref())
1347 .map(|o| o == origin)
1348 .unwrap_or(false)
1349 })
1350 .count()
1351 }
1352
1353 #[allow(clippy::result_large_err)]
1357 pub async fn check_subscription_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1358 let context = {
1359 let Some(client) = self.clients.get(&client_id) else {
1360 return Ok(());
1361 };
1362 let current_subs = client.subscription_count().await;
1363
1364 if let Some(ref ctx) = client.auth_context {
1366 let max_subs = ctx.limits.max_subscriptions.or_else(|| {
1367 self.rate_limit_config
1368 .default_limits
1369 .as_ref()
1370 .and_then(|l| l.max_subscriptions)
1371 });
1372 if let Some(max_subs) = max_subs {
1373 if current_subs >= max_subs as usize {
1374 return Err(AuthDeny::new(
1375 crate::websocket::auth::AuthErrorCode::SubscriptionLimitExceeded,
1376 format!(
1377 "Subscription limit exceeded: {} of {} subscriptions for client {}",
1378 current_subs, max_subs, client_id
1379 ),
1380 )
1381 .with_suggested_action(
1382 "Unsubscribe from an existing view before creating another subscription",
1383 ));
1384 }
1385 }
1386 }
1387 client.auth_context.clone()
1388 };
1389
1390 if let (Some(rate_limiter), Some(ctx)) = (self.rate_limiter.as_ref(), context.as_ref()) {
1393 if !ctx.is_legacy_policy() {
1394 if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1395 .check_subscription_create_for_consumer(
1396 ctx.consumer_key(),
1397 ctx.limits.max_subscription_creates_per_minute,
1398 )
1399 .await
1400 {
1401 return Err(AuthDeny::rate_limited(retry_after, "subscription creates")
1402 .with_context(format!(
1403 "subscription-create rate limit for consumer {} of {} per minute exceeded",
1404 ctx.consumer_key(),
1405 limit
1406 )));
1407 }
1408
1409 if ctx.account_key.is_some() {
1410 if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1411 .check_subscription_create_for_account(
1412 ctx.account_key(),
1413 ctx.account_limits.max_subscription_creates_per_minute,
1414 )
1415 .await
1416 {
1417 return Err(AuthDeny::rate_limited(
1418 retry_after,
1419 "account subscription creates",
1420 )
1421 .with_context(format!(
1422 "subscription-create rate limit for account {} of {} per minute exceeded",
1423 ctx.account_key(),
1424 limit
1425 )));
1426 }
1427 }
1428 }
1429 }
1430 Ok(())
1431 }
1432
1433 pub fn get_metering_key(&self, client_id: Uuid) -> Option<String> {
1435 self.clients.get(&client_id).and_then(|client| {
1436 client
1437 .auth_context
1438 .as_ref()
1439 .map(|ctx| ctx.metering_key.clone())
1440 })
1441 }
1442
1443 pub fn get_auth_context(&self, client_id: Uuid) -> Option<AuthContext> {
1445 self.clients
1446 .get(&client_id)
1447 .and_then(|client| client.auth_context.clone())
1448 }
1449
1450 #[allow(clippy::result_large_err)]
1454 pub fn check_snapshot_allowed(
1455 &self,
1456 client_id: Uuid,
1457 requested_rows: u32,
1458 ) -> Result<(), AuthDeny> {
1459 if let Some(client) = self.clients.get(&client_id) {
1460 if let Some(ref ctx) = client.auth_context {
1461 let max_rows = ctx.limits.max_snapshot_rows.or_else(|| {
1462 self.rate_limit_config
1463 .default_limits
1464 .as_ref()
1465 .and_then(|l| l.max_snapshot_rows)
1466 });
1467 if let Some(max_rows) = max_rows {
1468 if requested_rows > max_rows {
1469 return Err(AuthDeny::new(
1470 crate::websocket::auth::AuthErrorCode::SnapshotLimitExceeded,
1471 format!(
1472 "Snapshot limit exceeded: requested {} rows, max allowed is {} for client {}",
1473 requested_rows, max_rows, client_id
1474 ),
1475 )
1476 .with_suggested_action(
1477 "Request fewer rows or lower the snapshotLimit on the subscription",
1478 ));
1479 }
1480 }
1481 }
1482 }
1483 Ok(())
1484 }
1485}
1486
1487impl Default for ClientManager {
1488 fn default() -> Self {
1489 Self::new()
1490 }
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use super::*;
1496 use crate::websocket::auth::AuthContext;
1497 use arete_auth::{KeyClass, Limits};
1498 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1499
1500 fn create_test_auth_context(subject: &str, limits: Limits) -> AuthContext {
1501 AuthContext {
1502 subject: subject.to_string(),
1503 issuer: "test-issuer".to_string(),
1504 audience: "test-audience".to_string(),
1505 key_class: KeyClass::Publishable,
1506 metering_key: format!("meter-{}", subject),
1507 deployment_id: None,
1508 target_kind: None,
1509 target_id: None,
1510 program_id: None,
1511 program_release_hash: None,
1512 expires_at: u64::MAX,
1513 scope: "read".to_string(),
1514 limits,
1515 plan: None,
1516 origin: None,
1517 client_ip: None,
1518 jti: uuid::Uuid::new_v4().to_string(),
1519 actor_key: None,
1520 account_key: None,
1521 consumer_key: None,
1522 policy_version: None,
1523 account_limits: Limits::default(),
1524 }
1525 }
1526
1527 fn create_v2_auth_context(
1528 consumer: &str,
1529 account: &str,
1530 policy_version: u32,
1531 limits: Limits,
1532 account_limits: Limits,
1533 ) -> AuthContext {
1534 AuthContext {
1535 subject: "user:1".to_string(),
1536 issuer: "test-issuer".to_string(),
1537 audience: "test-audience".to_string(),
1538 key_class: KeyClass::Publishable,
1539 metering_key: account.to_string(),
1540 deployment_id: None,
1541 target_kind: None,
1542 target_id: None,
1543 program_id: None,
1544 program_release_hash: None,
1545 expires_at: u64::MAX,
1546 scope: "read".to_string(),
1547 limits,
1548 plan: Some("pro".to_string()),
1549 origin: None,
1550 client_ip: None,
1551 jti: uuid::Uuid::new_v4().to_string(),
1552 actor_key: Some("user:1".to_string()),
1553 account_key: Some(account.to_string()),
1554 consumer_key: Some(consumer.to_string()),
1555 policy_version: Some(policy_version),
1556 account_limits,
1557 }
1558 }
1559
1560 fn insert_client(manager: &ClientManager, context: AuthContext) -> Uuid {
1561 let (sender, receiver) = mpsc::channel(8);
1562 std::mem::forget(receiver);
1564 let client_id = Uuid::new_v4();
1565 manager.clients.insert(
1566 client_id,
1567 ClientInfo::new(
1568 client_id,
1569 sender,
1570 Some(context),
1571 create_test_socket_addr("127.0.0.1"),
1572 ),
1573 );
1574 client_id
1575 }
1576
1577 fn create_test_socket_addr(ip: &str) -> SocketAddr {
1578 SocketAddr::new(
1579 ip.parse::<IpAddr>()
1580 .unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1581 12345,
1582 )
1583 }
1584
1585 #[test]
1586 fn test_egress_tracker_basic() {
1587 let mut tracker = EgressTracker::new();
1588
1589 assert!(tracker.record_bytes(500, 1000));
1591 assert_eq!(tracker.current_usage(), 500);
1592
1593 assert!(tracker.record_bytes(400, 1000));
1595 assert_eq!(tracker.current_usage(), 900);
1596
1597 assert!(!tracker.record_bytes(200, 1000));
1599 assert_eq!(tracker.current_usage(), 900); }
1601
1602 #[test]
1603 fn test_egress_tracker_window_reset() {
1604 let mut tracker = EgressTracker::new();
1605
1606 assert!(tracker.record_bytes(100, 100));
1608 assert!(!tracker.record_bytes(1, 100));
1609
1610 tracker.bytes_this_minute = 0;
1612 tracker.window_start = SystemTime::now() - Duration::from_secs(61);
1613
1614 assert!(tracker.record_bytes(50, 100));
1616 }
1617
1618 #[test]
1619 fn test_message_rate_tracker_basic() {
1620 let mut tracker = MessageRateTracker::new();
1621
1622 assert!(tracker.record_message(2));
1623 assert_eq!(tracker.current_usage(), 1);
1624
1625 assert!(tracker.record_message(2));
1626 assert_eq!(tracker.current_usage(), 2);
1627
1628 assert!(!tracker.record_message(2));
1629 assert_eq!(tracker.current_usage(), 2);
1630 }
1631
1632 #[tokio::test]
1633 async fn test_client_inbound_message_limit() {
1634 let (tx, _rx) = mpsc::channel(1);
1635 let client = ClientInfo::new(
1636 Uuid::new_v4(),
1637 tx,
1638 Some(create_test_auth_context(
1639 "user-1",
1640 Limits {
1641 max_messages_per_minute: Some(2),
1642 ..Default::default()
1643 },
1644 )),
1645 create_test_socket_addr("127.0.0.1"),
1646 );
1647
1648 assert_eq!(client.record_inbound_message(), Some(1));
1649 assert_eq!(client.record_inbound_message(), Some(2));
1650 assert_eq!(client.record_inbound_message(), None);
1651 }
1652
1653 #[tokio::test]
1654 async fn duplicate_subscription_id_is_rejected_without_replacement() {
1655 let (tx, _rx) = mpsc::channel(1);
1656 let client = ClientInfo::new(
1657 Uuid::new_v4(),
1658 tx,
1659 None,
1660 create_test_socket_addr("127.0.0.1"),
1661 );
1662 let first = CancellationToken::new();
1663 let duplicate = CancellationToken::new();
1664
1665 assert!(
1666 client
1667 .add_subscription("opaque-id".to_string(), first.clone())
1668 .await
1669 );
1670 assert!(
1671 !client
1672 .add_subscription("opaque-id".to_string(), duplicate.clone())
1673 .await
1674 );
1675 assert!(!first.is_cancelled());
1676 assert!(!duplicate.is_cancelled());
1677
1678 assert!(client.remove_subscription("opaque-id").await);
1679 assert!(first.is_cancelled());
1680 assert!(!duplicate.is_cancelled());
1681 }
1682
1683 #[tokio::test]
1684 async fn test_no_limits() {
1685 let manager = ClientManager::new();
1686 let addr = create_test_socket_addr("127.0.0.1");
1687
1688 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1690
1691 let auth_context = create_test_auth_context("test", Limits::default());
1693 assert!(manager
1694 .check_connection_allowed(addr, &Some(auth_context))
1695 .await
1696 .is_ok());
1697 }
1698
1699 #[tokio::test]
1700 async fn test_per_subject_connection_limit() {
1701 let manager = ClientManager::new();
1702
1703 let limits = Limits {
1704 max_connections: Some(2),
1705 ..Default::default()
1706 };
1707
1708 let auth_context = create_test_auth_context("user-1", limits);
1709 let addr = create_test_socket_addr("127.0.0.1");
1710
1711 assert!(manager
1713 .check_connection_allowed(addr, &Some(auth_context.clone()))
1714 .await
1715 .is_ok());
1716 }
1717
1718 #[tokio::test]
1719 async fn test_per_ip_connection_limit() {
1720 let manager = ClientManager::new().with_max_connections_per_ip(2);
1721 let addr = create_test_socket_addr("192.168.1.1");
1722
1723 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1725 }
1726
1727 #[test]
1729 fn rate_limit_config_default() {
1730 let config = RateLimitConfig::default();
1731 assert!(config.max_connections_per_ip.is_none());
1732 assert_eq!(config.client_timeout, Duration::from_secs(300));
1733 assert_eq!(config.message_queue_size, 512);
1734 assert!(config.max_reconnect_attempts.is_none());
1735 assert_eq!(config.message_rate_window, Duration::from_secs(60));
1736 assert_eq!(config.egress_rate_window, Duration::from_secs(60));
1737 }
1738
1739 #[test]
1740 fn rate_limit_config_builder_methods() {
1741 let config = RateLimitConfig::default()
1742 .with_max_connections_per_ip(10)
1743 .with_timeout(Duration::from_secs(600))
1744 .with_message_queue_size(1024)
1745 .with_rate_limit_window(Duration::from_secs(120));
1746
1747 assert_eq!(config.max_connections_per_ip, Some(10));
1748 assert_eq!(config.client_timeout, Duration::from_secs(600));
1749 assert_eq!(config.message_queue_size, 1024);
1750 assert_eq!(config.message_rate_window, Duration::from_secs(120));
1751 assert_eq!(config.egress_rate_window, Duration::from_secs(120));
1752 }
1753
1754 #[tokio::test]
1755 async fn client_manager_with_config() {
1756 let config = RateLimitConfig::default()
1757 .with_max_connections_per_ip(5)
1758 .with_timeout(Duration::from_secs(120))
1759 .with_message_queue_size(256);
1760
1761 let manager = ClientManager::with_config(config);
1762 let addr = create_test_socket_addr("10.0.0.1");
1763
1764 assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(5));
1766 assert_eq!(
1767 manager.rate_limit_config().client_timeout,
1768 Duration::from_secs(120)
1769 );
1770 assert_eq!(manager.rate_limit_config().message_queue_size, 256);
1771
1772 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1774 }
1775
1776 #[tokio::test]
1777 async fn client_manager_builder_pattern() {
1778 let manager = ClientManager::new()
1779 .with_max_connections_per_ip(10)
1780 .with_timeout(Duration::from_secs(180))
1781 .with_message_queue_size(1024)
1782 .with_rate_limit_window(Duration::from_secs(90));
1783
1784 assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(10));
1785 assert_eq!(
1786 manager.rate_limit_config().client_timeout,
1787 Duration::from_secs(180)
1788 );
1789 assert_eq!(manager.rate_limit_config().message_queue_size, 1024);
1790 assert_eq!(
1791 manager.rate_limit_config().message_rate_window,
1792 Duration::from_secs(90)
1793 );
1794 }
1795
1796 #[tokio::test]
1798 async fn connection_limit_enforcement_with_actual_clients() {
1799 let manager = ClientManager::new().with_max_connections_per_ip(2);
1800 let addr1 = create_test_socket_addr("192.168.1.1");
1801 let addr2 = create_test_socket_addr("192.168.1.2");
1802
1803 let auth1 = create_test_auth_context("user-1", Limits::default());
1805 assert!(manager
1806 .check_connection_allowed(addr1, &Some(auth1.clone()))
1807 .await
1808 .is_ok());
1809
1810 let auth2 = create_test_auth_context("user-2", Limits::default());
1815 assert!(manager
1816 .check_connection_allowed(addr1, &Some(auth2.clone()))
1817 .await
1818 .is_ok());
1819
1820 let auth3 = create_test_auth_context("user-3", Limits::default());
1822 assert!(manager
1823 .check_connection_allowed(addr2, &Some(auth3.clone()))
1824 .await
1825 .is_ok());
1826 }
1827
1828 #[tokio::test]
1830 async fn subscription_limit_enforcement() {
1831 let manager = ClientManager::new();
1832 let addr = create_test_socket_addr("127.0.0.1");
1833
1834 let auth = create_test_auth_context(
1836 "user-1",
1837 Limits {
1838 max_subscriptions: Some(2),
1839 ..Default::default()
1840 },
1841 );
1842
1843 assert!(manager
1845 .check_connection_allowed(addr, &Some(auth.clone()))
1846 .await
1847 .is_ok());
1848
1849 assert_eq!(auth.limits.max_subscriptions, Some(2));
1852 }
1853
1854 #[tokio::test]
1856 async fn snapshot_limit_enforcement() {
1857 let manager = ClientManager::new();
1858 let addr = create_test_socket_addr("127.0.0.1");
1859
1860 let auth = create_test_auth_context(
1861 "user-1",
1862 Limits {
1863 max_snapshot_rows: Some(1000),
1864 ..Default::default()
1865 },
1866 );
1867
1868 assert!(manager
1869 .check_connection_allowed(addr, &Some(auth.clone()))
1870 .await
1871 .is_ok());
1872
1873 }
1876
1877 #[tokio::test]
1878 async fn two_consumers_share_the_account_connection_cap() {
1879 let manager = ClientManager::new();
1880 let account_limits = Limits {
1881 max_connections: Some(1),
1882 ..Limits::default()
1883 };
1884 let consumer_a = create_v2_auth_context(
1885 "consumer:a",
1886 "account:42",
1887 1,
1888 Limits::default(),
1889 account_limits.clone(),
1890 );
1891 insert_client(&manager, consumer_a);
1892
1893 let consumer_b = create_v2_auth_context(
1896 "consumer:b",
1897 "account:42",
1898 1,
1899 Limits::default(),
1900 account_limits.clone(),
1901 );
1902 let addr = create_test_socket_addr("127.0.0.1");
1903 assert!(manager
1904 .check_connection_allowed(addr, &Some(consumer_b))
1905 .await
1906 .is_err());
1907
1908 let other_account = create_v2_auth_context(
1910 "consumer:c",
1911 "account:43",
1912 1,
1913 Limits::default(),
1914 account_limits,
1915 );
1916 assert!(manager
1917 .check_connection_allowed(addr, &Some(other_account))
1918 .await
1919 .is_ok());
1920 }
1921
1922 #[tokio::test]
1923 async fn one_consumer_is_limited_independently_of_its_account() {
1924 let manager = ClientManager::new();
1925 let limits = Limits {
1926 max_connections: Some(1),
1927 ..Limits::default()
1928 };
1929 let account_limits = Limits {
1930 max_connections: Some(10),
1931 ..Limits::default()
1932 };
1933 let consumer_a = create_v2_auth_context(
1934 "consumer:a",
1935 "account:42",
1936 1,
1937 limits.clone(),
1938 account_limits.clone(),
1939 );
1940 insert_client(&manager, consumer_a.clone());
1941
1942 let addr = create_test_socket_addr("127.0.0.1");
1943 assert!(manager
1945 .check_connection_allowed(addr, &Some(consumer_a))
1946 .await
1947 .is_err());
1948
1949 let consumer_b =
1951 create_v2_auth_context("consumer:b", "account:42", 1, limits, account_limits);
1952 assert!(manager
1953 .check_connection_allowed(addr, &Some(consumer_b))
1954 .await
1955 .is_ok());
1956 }
1957
1958 #[tokio::test]
1959 async fn policy_version_upgrade_stale_and_conflict_rules_apply() {
1960 let manager = ClientManager::new();
1961 let addr = create_test_socket_addr("127.0.0.1");
1962 let limits_v1 = Limits {
1963 max_connections: Some(5),
1964 ..Limits::default()
1965 };
1966 let limits_v2 = Limits {
1967 max_connections: Some(2),
1968 ..Limits::default()
1969 };
1970
1971 let v1 = create_v2_auth_context(
1973 "consumer:a",
1974 "account:42",
1975 1,
1976 Limits::default(),
1977 limits_v1.clone(),
1978 );
1979 assert!(manager
1980 .check_connection_allowed(addr, &Some(v1.clone()))
1981 .await
1982 .is_ok());
1983
1984 let v2 = create_v2_auth_context(
1986 "consumer:a",
1987 "account:42",
1988 2,
1989 Limits::default(),
1990 limits_v2.clone(),
1991 );
1992 assert!(manager
1993 .check_connection_allowed(addr, &Some(v2))
1994 .await
1995 .is_ok());
1996
1997 let stale = manager.check_connection_allowed(addr, &Some(v1)).await;
1999 assert!(stale.is_err());
2000
2001 let conflict =
2003 create_v2_auth_context("consumer:a", "account:42", 2, Limits::default(), limits_v1);
2004 assert!(manager
2005 .check_connection_allowed(addr, &Some(conflict))
2006 .await
2007 .is_err());
2008 }
2009
2010 #[tokio::test]
2011 async fn legacy_tokens_keep_old_behavior_and_are_counted() {
2012 let manager = ClientManager::new();
2013 let addr = create_test_socket_addr("127.0.0.1");
2014 let legacy = create_test_auth_context(
2015 "user-1",
2016 Limits {
2017 max_connections: Some(2),
2018 ..Limits::default()
2019 },
2020 );
2021 assert!(legacy.is_legacy_policy());
2022
2023 assert!(manager
2024 .check_connection_allowed(addr, &Some(legacy.clone()))
2025 .await
2026 .is_ok());
2027 assert_eq!(manager.account_policies().legacy_token_count(), 1);
2028 assert_eq!(manager.account_policies().tracked_accounts(), 0);
2030
2031 insert_client(&manager, legacy.clone());
2033 insert_client(&manager, legacy.clone());
2034 assert!(manager
2035 .check_connection_allowed(addr, &Some(legacy))
2036 .await
2037 .is_err());
2038 }
2039
2040 #[tokio::test]
2041 async fn account_message_budget_is_shared_across_consumers() {
2042 let manager = ClientManager::new();
2043 let account_limits = Limits {
2044 max_messages_per_minute: Some(2),
2045 ..Limits::default()
2046 };
2047 let client_a = insert_client(
2048 &manager,
2049 create_v2_auth_context(
2050 "consumer:a",
2051 "account:42",
2052 1,
2053 Limits::default(),
2054 account_limits.clone(),
2055 ),
2056 );
2057 let client_b = insert_client(
2058 &manager,
2059 create_v2_auth_context(
2060 "consumer:b",
2061 "account:42",
2062 1,
2063 Limits::default(),
2064 account_limits,
2065 ),
2066 );
2067
2068 assert!(manager.check_inbound_message_allowed(client_a).is_ok());
2069 assert!(manager.check_inbound_message_allowed(client_b).is_ok());
2070 assert!(manager.check_inbound_message_allowed(client_a).is_err());
2072 }
2073
2074 #[tokio::test]
2075 async fn account_state_evicts_when_idle_and_unreferenced() {
2076 let manager = ClientManager::new();
2077 let addr = create_test_socket_addr("127.0.0.1");
2078 let context = create_v2_auth_context(
2079 "consumer:a",
2080 "account:42",
2081 1,
2082 Limits::default(),
2083 Limits::default(),
2084 );
2085 assert!(manager
2086 .check_connection_allowed(addr, &Some(context))
2087 .await
2088 .is_ok());
2089 assert_eq!(manager.account_policies().tracked_accounts(), 1);
2090
2091 manager.account_policies().evict_idle(|_| false);
2094 assert_eq!(
2095 manager.account_policies().tracked_accounts(),
2096 1,
2097 "TTL keeps fresh entries"
2098 );
2099 manager.cleanup_account_state();
2100 assert_eq!(
2101 manager.account_policies().tracked_accounts(),
2102 1,
2103 "fresh entries survive sweep"
2104 );
2105 }
2106
2107 #[test]
2108 fn expired_token_is_removed_without_deadlocking() {
2109 let manager = ClientManager::new();
2110 let mut context = create_test_auth_context("user-1", Limits::default());
2111 context.expires_at = 1;
2112 let client_id = insert_client(&manager, context);
2113
2114 let (done_tx, done_rx) = std::sync::mpsc::channel();
2117 let sender = manager.clone();
2118 std::thread::spawn(move || {
2119 let result = sender.send_to_client(client_id, Arc::new(Bytes::from_static(b"update")));
2120 let _ = done_tx.send(result);
2121 });
2122
2123 let result = done_rx
2124 .recv_timeout(Duration::from_secs(5))
2125 .expect("send_to_client deadlocked on an expired token");
2126 assert_eq!(result, Err(SendError::ClientDisconnected));
2127 assert!(!manager.has_client(client_id));
2128 }
2129
2130 #[tokio::test]
2131 async fn an_expired_client_keeps_its_close_reason_when_its_queue_is_full() {
2132 let manager = ClientManager::new();
2133 let (sender, mut queue) = mpsc::channel(1);
2134 sender
2135 .try_send(Message::Text("backlog".into()))
2136 .expect("room for one message");
2137 let client_id = Uuid::new_v4();
2138 let mut context = create_test_auth_context("user-1", Limits::default());
2139 context.expires_at = 1;
2140 let client = ClientInfo::new(
2141 client_id,
2142 sender,
2143 Some(context),
2144 create_test_socket_addr("127.0.0.1"),
2145 );
2146 let close_frame = client.close_frame.clone();
2147 manager.clients.insert(client_id, client);
2148
2149 assert!(manager.check_and_remove_expired(client_id));
2150
2151 let frame = close_frame
2152 .get()
2153 .expect("the close reason is recorded even though the queue is full");
2154 assert_eq!(frame.code, CloseCode::Policy);
2155 assert_eq!(
2156 frame.reason.as_str(),
2157 "token-expired: Authentication token expired"
2158 );
2159 assert!(matches!(queue.recv().await, Some(Message::Text(_))));
2163 assert!(queue.recv().await.is_none());
2164 }
2165
2166 #[test]
2167 fn unexpired_token_is_kept() {
2168 let manager = ClientManager::new();
2169 let client_id = insert_client(
2170 &manager,
2171 create_test_auth_context("user-1", Limits::default()),
2172 );
2173
2174 assert!(!manager.check_and_remove_expired(client_id));
2175 assert!(manager.has_client(client_id));
2176 }
2177
2178 #[tokio::test]
2180 async fn test_rate_limiter_integration() {
2181 use crate::websocket::rate_limiter::{RateLimiterConfig, WebSocketRateLimiter};
2182
2183 let rate_limiter = Arc::new(WebSocketRateLimiter::new(RateLimiterConfig::default()));
2184 let manager = ClientManager::new().with_rate_limiter(rate_limiter);
2185 let addr = create_test_socket_addr("127.0.0.1");
2186
2187 let auth = create_test_auth_context("user-1", Limits::default());
2189 assert!(manager
2190 .check_connection_allowed(addr, &Some(auth))
2191 .await
2192 .is_ok());
2193 }
2194}