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) {
1028 let client_manager = self.clone();
1029
1030 tokio::spawn(async move {
1031 let mut interval = tokio::time::interval(Duration::from_secs(30));
1032
1033 loop {
1034 interval.tick().await;
1035 let removed = client_manager.cleanup_stale_clients();
1036 if removed > 0 {
1037 info!("Cleaned up {} stale clients", removed);
1038 }
1039 client_manager.cleanup_account_state();
1040 }
1041 });
1042 }
1043
1044 #[allow(clippy::result_large_err)]
1052 pub async fn check_connection_allowed(
1053 &self,
1054 remote_addr: SocketAddr,
1055 auth_context: &Option<AuthContext>,
1056 ) -> Result<(), AuthDeny> {
1057 if let Some(ref rate_limiter) = self.rate_limiter {
1059 match rate_limiter.check_handshake(remote_addr).await {
1061 RateLimitResult::Allowed { .. } => {}
1062 RateLimitResult::Denied { retry_after, limit } => {
1063 return Err(AuthDeny::rate_limited(retry_after, "websocket handshakes")
1064 .with_context(format!(
1065 "handshake rate limit of {} per minute exceeded for {}",
1066 limit, remote_addr
1067 )));
1068 }
1069 }
1070
1071 if let Some(ref ctx) = auth_context {
1072 match rate_limiter
1076 .check_connection_for_consumer(
1077 ctx.consumer_key(),
1078 ctx.limits.max_connection_attempts_per_minute,
1079 )
1080 .await
1081 {
1082 RateLimitResult::Allowed { .. } => {}
1083 RateLimitResult::Denied { retry_after, limit } => {
1084 return Err(AuthDeny::rate_limited(retry_after, "websocket connections")
1085 .with_context(format!(
1086 "connection rate limit for consumer {} of {} per minute exceeded",
1087 ctx.consumer_key(),
1088 limit
1089 )));
1090 }
1091 }
1092
1093 if ctx.is_legacy_policy() || ctx.account_key.is_some() {
1097 match rate_limiter
1098 .check_connection_for_account(
1099 ctx.account_key(),
1100 ctx.account_limits.max_connection_attempts_per_minute,
1101 )
1102 .await
1103 {
1104 RateLimitResult::Allowed { .. } => {}
1105 RateLimitResult::Denied { retry_after, limit } => {
1106 return Err(AuthDeny::rate_limited(
1107 retry_after,
1108 "metered websocket connections",
1109 )
1110 .with_context(format!(
1111 "connection rate limit for account {} of {} per minute exceeded",
1112 ctx.account_key(),
1113 limit
1114 )));
1115 }
1116 }
1117 }
1118 }
1119 }
1120
1121 if let Some(max_per_ip) = self.rate_limit_config.max_connections_per_ip {
1123 let current_ip_connections = self.count_connections_for_ip(&remote_addr);
1124 if current_ip_connections >= max_per_ip {
1125 return Err(AuthDeny::connection_limit_exceeded(
1126 &format!("ip {}", remote_addr.ip()),
1127 current_ip_connections,
1128 max_per_ip,
1129 ));
1130 }
1131 }
1132
1133 if let Some(ctx) = auth_context {
1134 if ctx.is_legacy_policy() {
1137 let legacy_policy_token = self.account_policies.record_legacy_token();
1138 debug!(legacy_policy_token, "legacy policy token admitted");
1139 } else if let (Some(account), Some(policy_version)) =
1140 (ctx.account_key.as_deref(), ctx.policy_version)
1141 {
1142 match self
1143 .account_policies
1144 .observe(account, policy_version, &ctx.account_limits)
1145 {
1146 Ok(()) => {}
1147 Err(AccountPolicyError::StaleVersion { presented, current }) => {
1148 debug!(
1149 account = %redact_identity(account),
1150 presented,
1151 current,
1152 "stale policy version rejected"
1153 );
1154 return Err(AuthDeny::new(
1155 crate::websocket::auth::AuthErrorCode::TokenExpired,
1156 "session policy version is stale; refresh the session token",
1157 ));
1158 }
1159 Err(AccountPolicyError::ConflictingLimits { version }) => {
1160 warn!(
1161 account = %redact_identity(account),
1162 version,
1163 "signed account limits conflict for one policy version"
1164 );
1165 return Err(AuthDeny::new(
1166 crate::websocket::auth::AuthErrorCode::InternalError,
1167 "signed account limits conflict with previously observed policy",
1168 ));
1169 }
1170 Err(AccountPolicyError::CapacityExhausted) => {
1171 warn!("account policy state at capacity; denying admission");
1172 return Err(AuthDeny::new(
1173 crate::websocket::auth::AuthErrorCode::InternalError,
1174 "account policy state is at capacity; retry shortly",
1175 ));
1176 }
1177 }
1178 }
1179
1180 let max_connections = ctx.limits.max_connections.or_else(|| {
1184 self.rate_limit_config
1185 .default_limits
1186 .as_ref()
1187 .and_then(|l| l.max_connections)
1188 });
1189 if let Some(max_connections) = max_connections {
1190 let current_connections = self.count_connections_for_consumer(ctx.consumer_key());
1191 if current_connections >= max_connections as usize {
1192 return Err(AuthDeny::connection_limit_exceeded(
1193 &format!("consumer {}", ctx.consumer_key()),
1194 current_connections,
1195 max_connections as usize,
1196 ));
1197 }
1198 }
1199
1200 if !ctx.is_legacy_policy() && ctx.account_key.is_some() {
1203 if let Some(max_account_connections) = ctx.account_limits.max_connections {
1204 let current_account_connections =
1205 self.count_connections_for_account(ctx.account_key());
1206 if current_account_connections >= max_account_connections as usize {
1207 return Err(AuthDeny::connection_limit_exceeded(
1208 &format!("account {}", ctx.account_key()),
1209 current_account_connections,
1210 max_account_connections as usize,
1211 ));
1212 }
1213 }
1214 }
1215
1216 if let Some(max_per_metering_key) =
1218 self.rate_limit_config.max_connections_per_metering_key
1219 {
1220 let current_metering_connections =
1221 self.count_connections_for_metering_key(&ctx.metering_key);
1222 if current_metering_connections >= max_per_metering_key {
1223 return Err(AuthDeny::connection_limit_exceeded(
1224 &format!("metering key {}", ctx.metering_key),
1225 current_metering_connections,
1226 max_per_metering_key,
1227 ));
1228 }
1229 }
1230
1231 if let Some(max_per_origin) = self.rate_limit_config.max_connections_per_origin {
1233 if let Some(ref origin) = ctx.origin {
1234 let current_origin_connections = self.count_connections_for_origin(origin);
1235 if current_origin_connections >= max_per_origin {
1236 return Err(AuthDeny::connection_limit_exceeded(
1237 &format!("origin {}", origin),
1238 current_origin_connections,
1239 max_per_origin,
1240 ));
1241 }
1242 }
1243 }
1244 }
1245 Ok(())
1246 }
1247
1248 fn count_connections_for_ip(&self, remote_addr: &SocketAddr) -> usize {
1250 let ip = remote_addr.ip();
1251 self.clients
1252 .iter()
1253 .filter(|entry| entry.value().remote_addr.ip() == ip)
1254 .count()
1255 }
1256
1257 fn count_connections_for_consumer(&self, consumer: &str) -> usize {
1259 self.clients
1260 .iter()
1261 .filter(|entry| {
1262 entry
1263 .value()
1264 .auth_context
1265 .as_ref()
1266 .map(|ctx| ctx.consumer_key() == consumer)
1267 .unwrap_or(false)
1268 })
1269 .count()
1270 }
1271
1272 fn count_connections_for_account(&self, account: &str) -> usize {
1274 self.clients
1275 .iter()
1276 .filter(|entry| {
1277 entry
1278 .value()
1279 .auth_context
1280 .as_ref()
1281 .map(|ctx| ctx.account_key() == account)
1282 .unwrap_or(false)
1283 })
1284 .count()
1285 }
1286
1287 fn count_connections_for_metering_key(&self, metering_key: &str) -> usize {
1289 self.clients
1290 .iter()
1291 .filter(|entry| {
1292 entry
1293 .value()
1294 .auth_context
1295 .as_ref()
1296 .map(|ctx| ctx.metering_key == metering_key)
1297 .unwrap_or(false)
1298 })
1299 .count()
1300 }
1301
1302 fn count_connections_for_origin(&self, origin: &str) -> usize {
1304 self.clients
1305 .iter()
1306 .filter(|entry| {
1307 entry
1308 .value()
1309 .auth_context
1310 .as_ref()
1311 .and_then(|ctx| ctx.origin.as_ref())
1312 .map(|o| o == origin)
1313 .unwrap_or(false)
1314 })
1315 .count()
1316 }
1317
1318 #[allow(clippy::result_large_err)]
1322 pub async fn check_subscription_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1323 let context = {
1324 let Some(client) = self.clients.get(&client_id) else {
1325 return Ok(());
1326 };
1327 let current_subs = client.subscription_count().await;
1328
1329 if let Some(ref ctx) = client.auth_context {
1331 let max_subs = ctx.limits.max_subscriptions.or_else(|| {
1332 self.rate_limit_config
1333 .default_limits
1334 .as_ref()
1335 .and_then(|l| l.max_subscriptions)
1336 });
1337 if let Some(max_subs) = max_subs {
1338 if current_subs >= max_subs as usize {
1339 return Err(AuthDeny::new(
1340 crate::websocket::auth::AuthErrorCode::SubscriptionLimitExceeded,
1341 format!(
1342 "Subscription limit exceeded: {} of {} subscriptions for client {}",
1343 current_subs, max_subs, client_id
1344 ),
1345 )
1346 .with_suggested_action(
1347 "Unsubscribe from an existing view before creating another subscription",
1348 ));
1349 }
1350 }
1351 }
1352 client.auth_context.clone()
1353 };
1354
1355 if let (Some(rate_limiter), Some(ctx)) = (self.rate_limiter.as_ref(), context.as_ref()) {
1358 if !ctx.is_legacy_policy() {
1359 if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1360 .check_subscription_create_for_consumer(
1361 ctx.consumer_key(),
1362 ctx.limits.max_subscription_creates_per_minute,
1363 )
1364 .await
1365 {
1366 return Err(AuthDeny::rate_limited(retry_after, "subscription creates")
1367 .with_context(format!(
1368 "subscription-create rate limit for consumer {} of {} per minute exceeded",
1369 ctx.consumer_key(),
1370 limit
1371 )));
1372 }
1373
1374 if ctx.account_key.is_some() {
1375 if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1376 .check_subscription_create_for_account(
1377 ctx.account_key(),
1378 ctx.account_limits.max_subscription_creates_per_minute,
1379 )
1380 .await
1381 {
1382 return Err(AuthDeny::rate_limited(
1383 retry_after,
1384 "account subscription creates",
1385 )
1386 .with_context(format!(
1387 "subscription-create rate limit for account {} of {} per minute exceeded",
1388 ctx.account_key(),
1389 limit
1390 )));
1391 }
1392 }
1393 }
1394 }
1395 Ok(())
1396 }
1397
1398 pub fn get_metering_key(&self, client_id: Uuid) -> Option<String> {
1400 self.clients.get(&client_id).and_then(|client| {
1401 client
1402 .auth_context
1403 .as_ref()
1404 .map(|ctx| ctx.metering_key.clone())
1405 })
1406 }
1407
1408 pub fn get_auth_context(&self, client_id: Uuid) -> Option<AuthContext> {
1410 self.clients
1411 .get(&client_id)
1412 .and_then(|client| client.auth_context.clone())
1413 }
1414
1415 #[allow(clippy::result_large_err)]
1419 pub fn check_snapshot_allowed(
1420 &self,
1421 client_id: Uuid,
1422 requested_rows: u32,
1423 ) -> Result<(), AuthDeny> {
1424 if let Some(client) = self.clients.get(&client_id) {
1425 if let Some(ref ctx) = client.auth_context {
1426 let max_rows = ctx.limits.max_snapshot_rows.or_else(|| {
1427 self.rate_limit_config
1428 .default_limits
1429 .as_ref()
1430 .and_then(|l| l.max_snapshot_rows)
1431 });
1432 if let Some(max_rows) = max_rows {
1433 if requested_rows > max_rows {
1434 return Err(AuthDeny::new(
1435 crate::websocket::auth::AuthErrorCode::SnapshotLimitExceeded,
1436 format!(
1437 "Snapshot limit exceeded: requested {} rows, max allowed is {} for client {}",
1438 requested_rows, max_rows, client_id
1439 ),
1440 )
1441 .with_suggested_action(
1442 "Request fewer rows or lower the snapshotLimit on the subscription",
1443 ));
1444 }
1445 }
1446 }
1447 }
1448 Ok(())
1449 }
1450}
1451
1452impl Default for ClientManager {
1453 fn default() -> Self {
1454 Self::new()
1455 }
1456}
1457
1458#[cfg(test)]
1459mod tests {
1460 use super::*;
1461 use crate::websocket::auth::AuthContext;
1462 use arete_auth::{KeyClass, Limits};
1463 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1464
1465 fn create_test_auth_context(subject: &str, limits: Limits) -> AuthContext {
1466 AuthContext {
1467 subject: subject.to_string(),
1468 issuer: "test-issuer".to_string(),
1469 audience: "test-audience".to_string(),
1470 key_class: KeyClass::Publishable,
1471 metering_key: format!("meter-{}", subject),
1472 deployment_id: None,
1473 target_kind: None,
1474 target_id: None,
1475 program_id: None,
1476 program_release_hash: None,
1477 expires_at: u64::MAX,
1478 scope: "read".to_string(),
1479 limits,
1480 plan: None,
1481 origin: None,
1482 client_ip: None,
1483 jti: uuid::Uuid::new_v4().to_string(),
1484 actor_key: None,
1485 account_key: None,
1486 consumer_key: None,
1487 policy_version: None,
1488 account_limits: Limits::default(),
1489 }
1490 }
1491
1492 fn create_v2_auth_context(
1493 consumer: &str,
1494 account: &str,
1495 policy_version: u32,
1496 limits: Limits,
1497 account_limits: Limits,
1498 ) -> AuthContext {
1499 AuthContext {
1500 subject: "user:1".to_string(),
1501 issuer: "test-issuer".to_string(),
1502 audience: "test-audience".to_string(),
1503 key_class: KeyClass::Publishable,
1504 metering_key: account.to_string(),
1505 deployment_id: None,
1506 target_kind: None,
1507 target_id: None,
1508 program_id: None,
1509 program_release_hash: None,
1510 expires_at: u64::MAX,
1511 scope: "read".to_string(),
1512 limits,
1513 plan: Some("pro".to_string()),
1514 origin: None,
1515 client_ip: None,
1516 jti: uuid::Uuid::new_v4().to_string(),
1517 actor_key: Some("user:1".to_string()),
1518 account_key: Some(account.to_string()),
1519 consumer_key: Some(consumer.to_string()),
1520 policy_version: Some(policy_version),
1521 account_limits,
1522 }
1523 }
1524
1525 fn insert_client(manager: &ClientManager, context: AuthContext) -> Uuid {
1526 let (sender, receiver) = mpsc::channel(8);
1527 std::mem::forget(receiver);
1529 let client_id = Uuid::new_v4();
1530 manager.clients.insert(
1531 client_id,
1532 ClientInfo::new(
1533 client_id,
1534 sender,
1535 Some(context),
1536 create_test_socket_addr("127.0.0.1"),
1537 ),
1538 );
1539 client_id
1540 }
1541
1542 fn create_test_socket_addr(ip: &str) -> SocketAddr {
1543 SocketAddr::new(
1544 ip.parse::<IpAddr>()
1545 .unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1546 12345,
1547 )
1548 }
1549
1550 #[test]
1551 fn test_egress_tracker_basic() {
1552 let mut tracker = EgressTracker::new();
1553
1554 assert!(tracker.record_bytes(500, 1000));
1556 assert_eq!(tracker.current_usage(), 500);
1557
1558 assert!(tracker.record_bytes(400, 1000));
1560 assert_eq!(tracker.current_usage(), 900);
1561
1562 assert!(!tracker.record_bytes(200, 1000));
1564 assert_eq!(tracker.current_usage(), 900); }
1566
1567 #[test]
1568 fn test_egress_tracker_window_reset() {
1569 let mut tracker = EgressTracker::new();
1570
1571 assert!(tracker.record_bytes(100, 100));
1573 assert!(!tracker.record_bytes(1, 100));
1574
1575 tracker.bytes_this_minute = 0;
1577 tracker.window_start = SystemTime::now() - Duration::from_secs(61);
1578
1579 assert!(tracker.record_bytes(50, 100));
1581 }
1582
1583 #[test]
1584 fn test_message_rate_tracker_basic() {
1585 let mut tracker = MessageRateTracker::new();
1586
1587 assert!(tracker.record_message(2));
1588 assert_eq!(tracker.current_usage(), 1);
1589
1590 assert!(tracker.record_message(2));
1591 assert_eq!(tracker.current_usage(), 2);
1592
1593 assert!(!tracker.record_message(2));
1594 assert_eq!(tracker.current_usage(), 2);
1595 }
1596
1597 #[tokio::test]
1598 async fn test_client_inbound_message_limit() {
1599 let (tx, _rx) = mpsc::channel(1);
1600 let client = ClientInfo::new(
1601 Uuid::new_v4(),
1602 tx,
1603 Some(create_test_auth_context(
1604 "user-1",
1605 Limits {
1606 max_messages_per_minute: Some(2),
1607 ..Default::default()
1608 },
1609 )),
1610 create_test_socket_addr("127.0.0.1"),
1611 );
1612
1613 assert_eq!(client.record_inbound_message(), Some(1));
1614 assert_eq!(client.record_inbound_message(), Some(2));
1615 assert_eq!(client.record_inbound_message(), None);
1616 }
1617
1618 #[tokio::test]
1619 async fn duplicate_subscription_id_is_rejected_without_replacement() {
1620 let (tx, _rx) = mpsc::channel(1);
1621 let client = ClientInfo::new(
1622 Uuid::new_v4(),
1623 tx,
1624 None,
1625 create_test_socket_addr("127.0.0.1"),
1626 );
1627 let first = CancellationToken::new();
1628 let duplicate = CancellationToken::new();
1629
1630 assert!(
1631 client
1632 .add_subscription("opaque-id".to_string(), first.clone())
1633 .await
1634 );
1635 assert!(
1636 !client
1637 .add_subscription("opaque-id".to_string(), duplicate.clone())
1638 .await
1639 );
1640 assert!(!first.is_cancelled());
1641 assert!(!duplicate.is_cancelled());
1642
1643 assert!(client.remove_subscription("opaque-id").await);
1644 assert!(first.is_cancelled());
1645 assert!(!duplicate.is_cancelled());
1646 }
1647
1648 #[tokio::test]
1649 async fn test_no_limits() {
1650 let manager = ClientManager::new();
1651 let addr = create_test_socket_addr("127.0.0.1");
1652
1653 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1655
1656 let auth_context = create_test_auth_context("test", Limits::default());
1658 assert!(manager
1659 .check_connection_allowed(addr, &Some(auth_context))
1660 .await
1661 .is_ok());
1662 }
1663
1664 #[tokio::test]
1665 async fn test_per_subject_connection_limit() {
1666 let manager = ClientManager::new();
1667
1668 let limits = Limits {
1669 max_connections: Some(2),
1670 ..Default::default()
1671 };
1672
1673 let auth_context = create_test_auth_context("user-1", limits);
1674 let addr = create_test_socket_addr("127.0.0.1");
1675
1676 assert!(manager
1678 .check_connection_allowed(addr, &Some(auth_context.clone()))
1679 .await
1680 .is_ok());
1681 }
1682
1683 #[tokio::test]
1684 async fn test_per_ip_connection_limit() {
1685 let manager = ClientManager::new().with_max_connections_per_ip(2);
1686 let addr = create_test_socket_addr("192.168.1.1");
1687
1688 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1690 }
1691
1692 #[test]
1694 fn rate_limit_config_default() {
1695 let config = RateLimitConfig::default();
1696 assert!(config.max_connections_per_ip.is_none());
1697 assert_eq!(config.client_timeout, Duration::from_secs(300));
1698 assert_eq!(config.message_queue_size, 512);
1699 assert!(config.max_reconnect_attempts.is_none());
1700 assert_eq!(config.message_rate_window, Duration::from_secs(60));
1701 assert_eq!(config.egress_rate_window, Duration::from_secs(60));
1702 }
1703
1704 #[test]
1705 fn rate_limit_config_builder_methods() {
1706 let config = RateLimitConfig::default()
1707 .with_max_connections_per_ip(10)
1708 .with_timeout(Duration::from_secs(600))
1709 .with_message_queue_size(1024)
1710 .with_rate_limit_window(Duration::from_secs(120));
1711
1712 assert_eq!(config.max_connections_per_ip, Some(10));
1713 assert_eq!(config.client_timeout, Duration::from_secs(600));
1714 assert_eq!(config.message_queue_size, 1024);
1715 assert_eq!(config.message_rate_window, Duration::from_secs(120));
1716 assert_eq!(config.egress_rate_window, Duration::from_secs(120));
1717 }
1718
1719 #[tokio::test]
1720 async fn client_manager_with_config() {
1721 let config = RateLimitConfig::default()
1722 .with_max_connections_per_ip(5)
1723 .with_timeout(Duration::from_secs(120))
1724 .with_message_queue_size(256);
1725
1726 let manager = ClientManager::with_config(config);
1727 let addr = create_test_socket_addr("10.0.0.1");
1728
1729 assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(5));
1731 assert_eq!(
1732 manager.rate_limit_config().client_timeout,
1733 Duration::from_secs(120)
1734 );
1735 assert_eq!(manager.rate_limit_config().message_queue_size, 256);
1736
1737 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
1739 }
1740
1741 #[tokio::test]
1742 async fn client_manager_builder_pattern() {
1743 let manager = ClientManager::new()
1744 .with_max_connections_per_ip(10)
1745 .with_timeout(Duration::from_secs(180))
1746 .with_message_queue_size(1024)
1747 .with_rate_limit_window(Duration::from_secs(90));
1748
1749 assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(10));
1750 assert_eq!(
1751 manager.rate_limit_config().client_timeout,
1752 Duration::from_secs(180)
1753 );
1754 assert_eq!(manager.rate_limit_config().message_queue_size, 1024);
1755 assert_eq!(
1756 manager.rate_limit_config().message_rate_window,
1757 Duration::from_secs(90)
1758 );
1759 }
1760
1761 #[tokio::test]
1763 async fn connection_limit_enforcement_with_actual_clients() {
1764 let manager = ClientManager::new().with_max_connections_per_ip(2);
1765 let addr1 = create_test_socket_addr("192.168.1.1");
1766 let addr2 = create_test_socket_addr("192.168.1.2");
1767
1768 let auth1 = create_test_auth_context("user-1", Limits::default());
1770 assert!(manager
1771 .check_connection_allowed(addr1, &Some(auth1.clone()))
1772 .await
1773 .is_ok());
1774
1775 let auth2 = create_test_auth_context("user-2", Limits::default());
1780 assert!(manager
1781 .check_connection_allowed(addr1, &Some(auth2.clone()))
1782 .await
1783 .is_ok());
1784
1785 let auth3 = create_test_auth_context("user-3", Limits::default());
1787 assert!(manager
1788 .check_connection_allowed(addr2, &Some(auth3.clone()))
1789 .await
1790 .is_ok());
1791 }
1792
1793 #[tokio::test]
1795 async fn subscription_limit_enforcement() {
1796 let manager = ClientManager::new();
1797 let addr = create_test_socket_addr("127.0.0.1");
1798
1799 let auth = create_test_auth_context(
1801 "user-1",
1802 Limits {
1803 max_subscriptions: Some(2),
1804 ..Default::default()
1805 },
1806 );
1807
1808 assert!(manager
1810 .check_connection_allowed(addr, &Some(auth.clone()))
1811 .await
1812 .is_ok());
1813
1814 assert_eq!(auth.limits.max_subscriptions, Some(2));
1817 }
1818
1819 #[tokio::test]
1821 async fn snapshot_limit_enforcement() {
1822 let manager = ClientManager::new();
1823 let addr = create_test_socket_addr("127.0.0.1");
1824
1825 let auth = create_test_auth_context(
1826 "user-1",
1827 Limits {
1828 max_snapshot_rows: Some(1000),
1829 ..Default::default()
1830 },
1831 );
1832
1833 assert!(manager
1834 .check_connection_allowed(addr, &Some(auth.clone()))
1835 .await
1836 .is_ok());
1837
1838 }
1841
1842 #[tokio::test]
1843 async fn two_consumers_share_the_account_connection_cap() {
1844 let manager = ClientManager::new();
1845 let account_limits = Limits {
1846 max_connections: Some(1),
1847 ..Limits::default()
1848 };
1849 let consumer_a = create_v2_auth_context(
1850 "consumer:a",
1851 "account:42",
1852 1,
1853 Limits::default(),
1854 account_limits.clone(),
1855 );
1856 insert_client(&manager, consumer_a);
1857
1858 let consumer_b = create_v2_auth_context(
1861 "consumer:b",
1862 "account:42",
1863 1,
1864 Limits::default(),
1865 account_limits.clone(),
1866 );
1867 let addr = create_test_socket_addr("127.0.0.1");
1868 assert!(manager
1869 .check_connection_allowed(addr, &Some(consumer_b))
1870 .await
1871 .is_err());
1872
1873 let other_account = create_v2_auth_context(
1875 "consumer:c",
1876 "account:43",
1877 1,
1878 Limits::default(),
1879 account_limits,
1880 );
1881 assert!(manager
1882 .check_connection_allowed(addr, &Some(other_account))
1883 .await
1884 .is_ok());
1885 }
1886
1887 #[tokio::test]
1888 async fn one_consumer_is_limited_independently_of_its_account() {
1889 let manager = ClientManager::new();
1890 let limits = Limits {
1891 max_connections: Some(1),
1892 ..Limits::default()
1893 };
1894 let account_limits = Limits {
1895 max_connections: Some(10),
1896 ..Limits::default()
1897 };
1898 let consumer_a = create_v2_auth_context(
1899 "consumer:a",
1900 "account:42",
1901 1,
1902 limits.clone(),
1903 account_limits.clone(),
1904 );
1905 insert_client(&manager, consumer_a.clone());
1906
1907 let addr = create_test_socket_addr("127.0.0.1");
1908 assert!(manager
1910 .check_connection_allowed(addr, &Some(consumer_a))
1911 .await
1912 .is_err());
1913
1914 let consumer_b =
1916 create_v2_auth_context("consumer:b", "account:42", 1, limits, account_limits);
1917 assert!(manager
1918 .check_connection_allowed(addr, &Some(consumer_b))
1919 .await
1920 .is_ok());
1921 }
1922
1923 #[tokio::test]
1924 async fn policy_version_upgrade_stale_and_conflict_rules_apply() {
1925 let manager = ClientManager::new();
1926 let addr = create_test_socket_addr("127.0.0.1");
1927 let limits_v1 = Limits {
1928 max_connections: Some(5),
1929 ..Limits::default()
1930 };
1931 let limits_v2 = Limits {
1932 max_connections: Some(2),
1933 ..Limits::default()
1934 };
1935
1936 let v1 = create_v2_auth_context(
1938 "consumer:a",
1939 "account:42",
1940 1,
1941 Limits::default(),
1942 limits_v1.clone(),
1943 );
1944 assert!(manager
1945 .check_connection_allowed(addr, &Some(v1.clone()))
1946 .await
1947 .is_ok());
1948
1949 let v2 = create_v2_auth_context(
1951 "consumer:a",
1952 "account:42",
1953 2,
1954 Limits::default(),
1955 limits_v2.clone(),
1956 );
1957 assert!(manager
1958 .check_connection_allowed(addr, &Some(v2))
1959 .await
1960 .is_ok());
1961
1962 let stale = manager.check_connection_allowed(addr, &Some(v1)).await;
1964 assert!(stale.is_err());
1965
1966 let conflict =
1968 create_v2_auth_context("consumer:a", "account:42", 2, Limits::default(), limits_v1);
1969 assert!(manager
1970 .check_connection_allowed(addr, &Some(conflict))
1971 .await
1972 .is_err());
1973 }
1974
1975 #[tokio::test]
1976 async fn legacy_tokens_keep_old_behavior_and_are_counted() {
1977 let manager = ClientManager::new();
1978 let addr = create_test_socket_addr("127.0.0.1");
1979 let legacy = create_test_auth_context(
1980 "user-1",
1981 Limits {
1982 max_connections: Some(2),
1983 ..Limits::default()
1984 },
1985 );
1986 assert!(legacy.is_legacy_policy());
1987
1988 assert!(manager
1989 .check_connection_allowed(addr, &Some(legacy.clone()))
1990 .await
1991 .is_ok());
1992 assert_eq!(manager.account_policies().legacy_token_count(), 1);
1993 assert_eq!(manager.account_policies().tracked_accounts(), 0);
1995
1996 insert_client(&manager, legacy.clone());
1998 insert_client(&manager, legacy.clone());
1999 assert!(manager
2000 .check_connection_allowed(addr, &Some(legacy))
2001 .await
2002 .is_err());
2003 }
2004
2005 #[tokio::test]
2006 async fn account_message_budget_is_shared_across_consumers() {
2007 let manager = ClientManager::new();
2008 let account_limits = Limits {
2009 max_messages_per_minute: Some(2),
2010 ..Limits::default()
2011 };
2012 let client_a = insert_client(
2013 &manager,
2014 create_v2_auth_context(
2015 "consumer:a",
2016 "account:42",
2017 1,
2018 Limits::default(),
2019 account_limits.clone(),
2020 ),
2021 );
2022 let client_b = insert_client(
2023 &manager,
2024 create_v2_auth_context(
2025 "consumer:b",
2026 "account:42",
2027 1,
2028 Limits::default(),
2029 account_limits,
2030 ),
2031 );
2032
2033 assert!(manager.check_inbound_message_allowed(client_a).is_ok());
2034 assert!(manager.check_inbound_message_allowed(client_b).is_ok());
2035 assert!(manager.check_inbound_message_allowed(client_a).is_err());
2037 }
2038
2039 #[tokio::test]
2040 async fn account_state_evicts_when_idle_and_unreferenced() {
2041 let manager = ClientManager::new();
2042 let addr = create_test_socket_addr("127.0.0.1");
2043 let context = create_v2_auth_context(
2044 "consumer:a",
2045 "account:42",
2046 1,
2047 Limits::default(),
2048 Limits::default(),
2049 );
2050 assert!(manager
2051 .check_connection_allowed(addr, &Some(context))
2052 .await
2053 .is_ok());
2054 assert_eq!(manager.account_policies().tracked_accounts(), 1);
2055
2056 manager.account_policies().evict_idle(|_| false);
2059 assert_eq!(
2060 manager.account_policies().tracked_accounts(),
2061 1,
2062 "TTL keeps fresh entries"
2063 );
2064 manager.cleanup_account_state();
2065 assert_eq!(
2066 manager.account_policies().tracked_accounts(),
2067 1,
2068 "fresh entries survive sweep"
2069 );
2070 }
2071
2072 #[tokio::test]
2074 async fn test_rate_limiter_integration() {
2075 use crate::websocket::rate_limiter::{RateLimiterConfig, WebSocketRateLimiter};
2076
2077 let rate_limiter = Arc::new(WebSocketRateLimiter::new(RateLimiterConfig::default()));
2078 let manager = ClientManager::new().with_rate_limiter(rate_limiter);
2079 let addr = create_test_socket_addr("127.0.0.1");
2080
2081 let auth = create_test_auth_context("user-1", Limits::default());
2083 assert!(manager
2084 .check_connection_allowed(addr, &Some(auth))
2085 .await
2086 .is_ok());
2087 }
2088}