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