1use crate::account_policy::{redact_identity, AccountPolicyError, AccountPolicyRegistry};
2use crate::compression::CompressedPayload;
3use crate::websocket::admission::{WebSocketAdmissionProvider, WebSocketConnectionPermit};
4use crate::websocket::auth::{AuthContext, AuthDeny, AuthErrorCode};
5use crate::websocket::rate_limiter::{RateLimitResult, WebSocketRateLimiter};
6use arete_auth::Limits;
7use bytes::Bytes;
8use dashmap::DashMap;
9use futures_util::stream::SplitSink;
10use futures_util::SinkExt;
11use std::collections::{HashMap, HashSet};
12use std::net::SocketAddr;
13use std::sync::Arc;
14use std::time::{Duration, Instant, SystemTime};
15use tokio::net::TcpStream;
16use tokio::sync::{mpsc, Mutex as AsyncMutex, RwLock};
17use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
18use tokio_tungstenite::tungstenite::protocol::CloseFrame;
19use tokio_tungstenite::{tungstenite::Message, WebSocketStream};
20use tokio_util::sync::CancellationToken;
21use tracing::{debug, info, warn};
22use uuid::Uuid;
23
24pub type WebSocketSender = SplitSink<WebSocketStream<TcpStream>, Message>;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum SendError {
29 ClientNotFound,
31 ClientBackpressured,
33 ClientDisconnected,
35}
36
37impl std::fmt::Display for SendError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 SendError::ClientNotFound => write!(f, "client not found"),
41 SendError::ClientBackpressured => write!(f, "client backpressured and disconnected"),
42 SendError::ClientDisconnected => write!(f, "client disconnected"),
43 }
44 }
45}
46
47impl std::error::Error for SendError {}
48
49#[derive(Debug)]
51struct EgressTracker {
52 bytes_this_minute: u64,
54 window_start: SystemTime,
56}
57
58#[derive(Debug)]
60struct MessageRateTracker {
61 messages_this_minute: u32,
62 window_start: SystemTime,
63}
64
65impl MessageRateTracker {
66 fn new() -> Self {
67 Self {
68 messages_this_minute: 0,
69 window_start: SystemTime::now(),
70 }
71 }
72
73 fn maybe_reset_window(&mut self) {
74 let now = SystemTime::now();
75 if now.duration_since(self.window_start).unwrap_or_default() >= Duration::from_secs(60) {
76 self.messages_this_minute = 0;
77 self.window_start = now;
78 }
79 }
80
81 fn record_message(&mut self, limit: u32) -> bool {
82 self.maybe_reset_window();
83 if self.messages_this_minute + 1 > limit {
84 false
85 } else {
86 self.messages_this_minute += 1;
87 true
88 }
89 }
90
91 fn current_usage(&mut self) -> u32 {
92 self.maybe_reset_window();
93 self.messages_this_minute
94 }
95}
96
97impl EgressTracker {
98 fn new() -> Self {
99 Self {
100 bytes_this_minute: 0,
101 window_start: SystemTime::now(),
102 }
103 }
104
105 fn maybe_reset_window(&mut self) {
107 let now = SystemTime::now();
108 if now.duration_since(self.window_start).unwrap_or_default() >= Duration::from_secs(60) {
109 self.bytes_this_minute = 0;
110 self.window_start = now;
111 }
112 }
113
114 fn record_bytes(&mut self, bytes: usize, limit: u64) -> bool {
116 self.maybe_reset_window();
117 let bytes_u64 = bytes as u64;
118 if self.bytes_this_minute + bytes_u64 > limit {
119 false
120 } else {
121 self.bytes_this_minute += bytes_u64;
122 true
123 }
124 }
125
126 fn current_usage(&mut self) -> u64 {
128 self.maybe_reset_window();
129 self.bytes_this_minute
130 }
131}
132
133#[derive(Debug)]
138struct AccountUsage {
139 egress: std::sync::Mutex<EgressTracker>,
140 messages: std::sync::Mutex<MessageRateTracker>,
141 last_seen: std::sync::Mutex<Instant>,
142}
143
144impl AccountUsage {
145 fn new() -> Self {
146 Self {
147 egress: std::sync::Mutex::new(EgressTracker::new()),
148 messages: std::sync::Mutex::new(MessageRateTracker::new()),
149 last_seen: std::sync::Mutex::new(Instant::now()),
150 }
151 }
152
153 fn touch(&self) {
154 if let Ok(mut last_seen) = self.last_seen.lock() {
155 *last_seen = Instant::now();
156 }
157 }
158
159 fn record_egress(&self, bytes: usize, limit: u64) -> bool {
160 self.touch();
161 match self.egress.lock() {
162 Ok(mut tracker) => tracker.record_bytes(bytes, limit),
163 Err(_) => true,
164 }
165 }
166
167 fn record_message(&self, limit: u32) -> bool {
168 self.touch();
169 match self.messages.lock() {
170 Ok(mut tracker) => tracker.record_message(limit),
171 Err(_) => true,
172 }
173 }
174
175 fn idle_for(&self) -> Duration {
176 self.last_seen
177 .lock()
178 .map(|last_seen| last_seen.elapsed())
179 .unwrap_or(Duration::MAX)
180 }
181}
182
183#[derive(Debug)]
185pub struct ClientInfo {
186 pub id: Uuid,
187 pub last_seen: SystemTime,
188 pub sender: mpsc::Sender<Message>,
189 subscriptions: Arc<RwLock<HashMap<String, CancellationToken>>>,
190 pub auth_context: Option<AuthContext>,
192 pub remote_addr: SocketAddr,
194 egress_tracker: std::sync::Mutex<EgressTracker>,
196 message_rate_tracker: std::sync::Mutex<MessageRateTracker>,
198 close_frame: Arc<std::sync::OnceLock<CloseFrame>>,
202 admission_permit: Option<Arc<dyn WebSocketConnectionPermit>>,
203 admission_lifecycle: Arc<AsyncMutex<()>>,
206}
207
208impl ClientInfo {
209 pub fn new(
210 id: Uuid,
211 sender: mpsc::Sender<Message>,
212 auth_context: Option<AuthContext>,
213 remote_addr: SocketAddr,
214 ) -> Self {
215 Self {
216 id,
217 last_seen: SystemTime::now(),
218 sender,
219 subscriptions: Arc::new(RwLock::new(HashMap::new())),
220 auth_context,
221 remote_addr,
222 egress_tracker: std::sync::Mutex::new(EgressTracker::new()),
223 message_rate_tracker: std::sync::Mutex::new(MessageRateTracker::new()),
224 close_frame: Arc::new(std::sync::OnceLock::new()),
225 admission_permit: None,
226 admission_lifecycle: Arc::new(AsyncMutex::new(())),
227 }
228 }
229
230 pub fn record_egress(&self, bytes: usize) -> Option<u64> {
232 if let Ok(mut tracker) = self.egress_tracker.lock() {
233 if let Some(ref ctx) = self.auth_context {
234 if let Some(limit) = ctx.limits.max_bytes_per_minute {
235 if tracker.record_bytes(bytes, limit) {
236 return Some(tracker.current_usage());
237 } else {
238 return None; }
240 }
241 }
242 return Some(tracker.current_usage());
244 }
245 None
246 }
247
248 pub fn record_inbound_message(&self) -> Option<u32> {
250 if let Ok(mut tracker) = self.message_rate_tracker.lock() {
251 if let Some(ref ctx) = self.auth_context {
252 if let Some(limit) = ctx.limits.max_messages_per_minute {
253 if tracker.record_message(limit) {
254 return Some(tracker.current_usage());
255 } else {
256 return None;
257 }
258 }
259 }
260
261 return Some(tracker.current_usage());
262 }
263
264 None
265 }
266
267 pub fn update_last_seen(&mut self) {
268 self.last_seen = SystemTime::now();
269 }
270
271 pub fn is_stale(&self, timeout: Duration) -> bool {
272 self.last_seen.elapsed().unwrap_or(Duration::MAX) > timeout
273 }
274
275 pub async fn add_subscription(
276 &self,
277 subscription_id: String,
278 token: CancellationToken,
279 ) -> bool {
280 let _lifecycle = self.admission_lifecycle.lock().await;
281 let mut subs = self.subscriptions.write().await;
282 match subs.entry(subscription_id) {
283 std::collections::hash_map::Entry::Vacant(entry) => {
284 entry.insert(token);
285 true
286 }
287 std::collections::hash_map::Entry::Occupied(_) => false,
288 }
289 }
290
291 pub async fn remove_subscription(&self, subscription_id: &str) -> bool {
292 let _lifecycle = self.admission_lifecycle.lock().await;
293 let mut subs = self.subscriptions.write().await;
294 if let Some(token) = subs.remove(subscription_id) {
295 token.cancel();
296 debug!("Cancelled subscription: {}", subscription_id);
297 true
298 } else {
299 debug!(
300 "Subscription not found for cancellation: {}",
301 subscription_id
302 );
303 false
304 }
305 }
306
307 pub async fn cancel_all_subscriptions(&self) {
308 let _lifecycle = self.admission_lifecycle.lock().await;
309 let subs = self.subscriptions.read().await;
310 for (subscription_id, token) in subs.iter() {
311 token.cancel();
312 debug!("Cancelled subscription on disconnect: {}", subscription_id);
313 }
314 }
315
316 pub async fn subscription_count(&self) -> usize {
317 self.subscriptions.read().await.len()
318 }
319}
320
321#[derive(Debug, Clone)]
326pub struct RateLimitConfig {
327 pub max_connections_per_ip: Option<usize>,
329 pub max_connections_per_metering_key: Option<usize>,
331 pub max_connections_per_origin: Option<usize>,
333 pub client_timeout: Duration,
335 pub message_queue_size: usize,
337 pub max_reconnect_attempts: Option<u32>,
339 pub message_rate_window: Duration,
341 pub egress_rate_window: Duration,
343 pub default_limits: Option<Limits>,
346}
347
348impl Default for RateLimitConfig {
349 fn default() -> Self {
350 Self {
351 max_connections_per_ip: None,
352 max_connections_per_metering_key: None,
353 max_connections_per_origin: None,
354 client_timeout: Duration::from_secs(300),
355 message_queue_size: 512,
356 max_reconnect_attempts: None,
357 message_rate_window: Duration::from_secs(60),
358 egress_rate_window: Duration::from_secs(60),
359 default_limits: None,
360 }
361 }
362}
363
364impl RateLimitConfig {
365 pub fn from_env() -> Self {
380 let mut config = Self::default();
381
382 if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_IP") {
383 if let Ok(max) = val.parse() {
384 config.max_connections_per_ip = Some(max);
385 }
386 }
387
388 if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_METERING_KEY") {
389 if let Ok(max) = val.parse() {
390 config.max_connections_per_metering_key = Some(max);
391 }
392 }
393
394 if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_ORIGIN") {
395 if let Ok(max) = val.parse() {
396 config.max_connections_per_origin = Some(max);
397 }
398 }
399
400 if let Ok(val) = std::env::var("ARETE_WS_CLIENT_TIMEOUT_SECS") {
401 if let Ok(secs) = val.parse() {
402 config.client_timeout = Duration::from_secs(secs);
403 }
404 }
405
406 if let Ok(val) = std::env::var("ARETE_WS_MESSAGE_QUEUE_SIZE") {
407 if let Ok(size) = val.parse() {
408 config.message_queue_size = size;
409 }
410 }
411
412 if let Ok(val) = std::env::var("ARETE_WS_RATE_LIMIT_WINDOW_SECS") {
413 if let Ok(secs) = val.parse() {
414 config.message_rate_window = Duration::from_secs(secs);
415 config.egress_rate_window = Duration::from_secs(secs);
416 }
417 }
418
419 let mut default_limits = Limits::default();
421 let mut has_default_limits = false;
422
423 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_CONNECTIONS") {
424 if let Ok(max) = val.parse() {
425 default_limits.max_connections = Some(max);
426 has_default_limits = true;
427 }
428 }
429
430 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SUBSCRIPTIONS") {
431 if let Ok(max) = val.parse() {
432 default_limits.max_subscriptions = Some(max);
433 has_default_limits = true;
434 }
435 }
436
437 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SNAPSHOT_ROWS") {
438 if let Ok(max) = val.parse() {
439 default_limits.max_snapshot_rows = Some(max);
440 has_default_limits = true;
441 }
442 }
443
444 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_MESSAGES_PER_MINUTE") {
445 if let Ok(max) = val.parse() {
446 default_limits.max_messages_per_minute = Some(max);
447 has_default_limits = true;
448 }
449 }
450
451 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_BYTES_PER_MINUTE") {
452 if let Ok(max) = val.parse() {
453 default_limits.max_bytes_per_minute = Some(max);
454 has_default_limits = true;
455 }
456 }
457
458 if has_default_limits {
459 config.default_limits = Some(default_limits);
460 }
461
462 config
463 }
464
465 pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
467 self.max_connections_per_ip = Some(max);
468 self
469 }
470
471 pub fn with_timeout(mut self, timeout: Duration) -> Self {
473 self.client_timeout = timeout;
474 self
475 }
476
477 pub fn with_message_queue_size(mut self, size: usize) -> Self {
479 self.message_queue_size = size;
480 self
481 }
482
483 pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
485 self.message_rate_window = window;
486 self.egress_rate_window = window;
487 self
488 }
489
490 pub fn with_default_limits(mut self, limits: Limits) -> Self {
495 self.default_limits = Some(limits);
496 self
497 }
498}
499
500const MAX_TRACKED_ACCOUNT_USAGE: usize = crate::account_policy::DEFAULT_MAX_TRACKED_ACCOUNTS;
503
504const ACCOUNT_USAGE_IDLE_TTL: Duration = crate::account_policy::DEFAULT_ACCOUNT_IDLE_TTL;
506
507#[derive(Clone)]
516pub struct ClientManager {
517 clients: Arc<DashMap<Uuid, ClientInfo>>,
518 rate_limit_config: RateLimitConfig,
519 rate_limiter: Option<Arc<WebSocketRateLimiter>>,
521 account_policies: Arc<AccountPolicyRegistry>,
523 account_usage: Arc<DashMap<String, AccountUsage>>,
525 admission_provider: Option<Arc<dyn WebSocketAdmissionProvider>>,
527}
528
529impl ClientManager {
530 pub fn new() -> Self {
531 Self::with_config(RateLimitConfig::default())
532 }
533
534 pub fn with_config(config: RateLimitConfig) -> Self {
536 Self {
537 clients: Arc::new(DashMap::new()),
538 rate_limit_config: config,
539 rate_limiter: None,
540 account_policies: Arc::new(AccountPolicyRegistry::default()),
541 account_usage: Arc::new(DashMap::new()),
542 admission_provider: None,
543 }
544 }
545
546 pub fn with_admission_provider(
548 mut self,
549 provider: Arc<dyn WebSocketAdmissionProvider>,
550 ) -> Self {
551 self.admission_provider = Some(provider);
552 self
553 }
554
555 #[allow(clippy::result_large_err)]
556 pub(crate) fn reserve_connection_admission(
557 &self,
558 context: &AuthContext,
559 ) -> Result<Option<Arc<dyn WebSocketConnectionPermit>>, AuthDeny> {
560 self.admission_provider
561 .as_ref()
562 .map(|provider| provider.reserve_connection(context))
563 .unwrap_or(Ok(None))
564 }
565
566 pub fn from_env() -> Self {
570 Self::with_config(RateLimitConfig::from_env())
571 }
572
573 pub fn with_timeout(mut self, timeout: Duration) -> Self {
575 self.rate_limit_config.client_timeout = timeout;
576 self
577 }
578
579 pub fn with_message_queue_size(mut self, queue_size: usize) -> Self {
581 self.rate_limit_config.message_queue_size = queue_size;
582 self
583 }
584
585 pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
587 self.rate_limit_config.max_connections_per_ip = Some(max);
588 self
589 }
590
591 pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
593 self.rate_limit_config.message_rate_window = window;
594 self.rate_limit_config.egress_rate_window = window;
595 self
596 }
597
598 pub fn with_default_limits(mut self, limits: Limits) -> Self {
603 self.rate_limit_config.default_limits = Some(limits);
604 self
605 }
606
607 pub fn with_rate_limiter(mut self, rate_limiter: Arc<WebSocketRateLimiter>) -> Self {
609 self.rate_limiter = Some(rate_limiter);
610 self
611 }
612
613 pub fn rate_limiter(&self) -> Option<&WebSocketRateLimiter> {
615 self.rate_limiter.as_ref().map(|r| r.as_ref())
616 }
617
618 pub fn rate_limit_config(&self) -> &RateLimitConfig {
620 &self.rate_limit_config
621 }
622
623 pub fn account_policies(&self) -> &AccountPolicyRegistry {
625 &self.account_policies
626 }
627
628 fn record_account_egress(&self, ctx: &AuthContext, bytes: usize) -> bool {
630 if ctx.is_legacy_policy() || ctx.account_key.is_none() {
631 return true;
632 }
633 let Some(limit) = ctx.account_limits.max_bytes_per_minute else {
634 return true;
635 };
636 let account = ctx.account_key();
637 if let Some(usage) = self.account_usage.get(account) {
638 return usage.record_egress(bytes, limit);
639 }
640 if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
641 warn!(
642 account = %redact_identity(account),
643 "account usage tracking at capacity; aggregate egress guard skipped"
644 );
645 return true;
646 }
647 self.account_usage
648 .entry(account.to_string())
649 .or_insert_with(AccountUsage::new)
650 .record_egress(bytes, limit)
651 }
652
653 fn record_account_message(&self, ctx: &AuthContext) -> bool {
655 if ctx.is_legacy_policy() || ctx.account_key.is_none() {
656 return true;
657 }
658 let Some(limit) = ctx.account_limits.max_messages_per_minute else {
659 return true;
660 };
661 let account = ctx.account_key();
662 if let Some(usage) = self.account_usage.get(account) {
663 return usage.record_message(limit);
664 }
665 if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
666 warn!(
667 account = %redact_identity(account),
668 "account usage tracking at capacity; aggregate message guard skipped"
669 );
670 return true;
671 }
672 self.account_usage
673 .entry(account.to_string())
674 .or_insert_with(AccountUsage::new)
675 .record_message(limit)
676 }
677
678 fn enforce_egress_budgets(&self, client_id: Uuid, bytes: usize) -> Result<(), SendError> {
680 let (connection_ok, account_ok, permit_id) = {
681 let Some(client) = self.clients.get(&client_id) else {
682 return Err(SendError::ClientNotFound);
683 };
684 let connection_ok = client.record_egress(bytes).is_some();
685 let account_ok = !connection_ok
686 || client
687 .auth_context
688 .as_ref()
689 .map(|ctx| self.record_account_egress(ctx, bytes))
690 .unwrap_or(true);
691 let permit_id = client.admission_permit.as_ref().map(|permit| permit.id());
692 (connection_ok, account_ok, permit_id)
693 };
694 if !connection_ok {
695 warn!("Client {} exceeded egress limit, disconnecting", client_id);
696 self.clients.remove(&client_id);
697 return Err(SendError::ClientDisconnected);
698 }
699 if !account_ok {
700 warn!(
701 "Client {} exceeded the account egress limit, disconnecting",
702 client_id
703 );
704 self.clients.remove(&client_id);
705 return Err(SendError::ClientDisconnected);
706 }
707 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
708 if let Err(deny) = provider.check_egress(permit_id, bytes) {
709 warn!(client = %client_id, reason = %deny.reason, "admission provider denied egress");
710 self.clients.remove(&client_id);
711 return Err(SendError::ClientDisconnected);
712 }
713 }
714 Ok(())
715 }
716
717 #[allow(clippy::result_large_err)]
723 pub fn add_client(
724 &self,
725 client_id: Uuid,
726 ws_sender: WebSocketSender,
727 auth_context: Option<AuthContext>,
728 remote_addr: SocketAddr,
729 ) -> Result<(), AuthDeny> {
730 let permit = if let Some(context) = auth_context.as_ref() {
731 self.reserve_connection_admission(context)?
732 } else {
733 None
734 };
735 self.add_client_with_admission(client_id, ws_sender, auth_context, remote_addr, permit);
736 Ok(())
737 }
738
739 pub(crate) fn add_client_with_admission(
740 &self,
741 client_id: Uuid,
742 mut ws_sender: WebSocketSender,
743 auth_context: Option<AuthContext>,
744 remote_addr: SocketAddr,
745 admission_permit: Option<Arc<dyn WebSocketConnectionPermit>>,
746 ) {
747 let (client_tx, mut client_rx) =
748 mpsc::channel::<Message>(self.rate_limit_config.message_queue_size);
749 let mut client_info = ClientInfo::new(client_id, client_tx, auth_context, remote_addr);
750 client_info.admission_permit = admission_permit;
751 let close_frame = client_info.close_frame.clone();
752
753 let clients_ref = self.clients.clone();
754 tokio::spawn(async move {
755 while let Some(message) = client_rx.recv().await {
756 if let Err(e) = ws_sender.send(message).await {
757 warn!("Failed to send message to client {}: {}", client_id, e);
758 break;
759 }
760 }
761 if let Some(frame) = close_frame.get() {
766 let _ = ws_sender.send(Message::Close(Some(frame.clone()))).await;
767 }
768 let _ = ws_sender.close().await;
769 clients_ref.remove(&client_id);
770 debug!("WebSocket sender task for client {} stopped", client_id);
771 });
772
773 self.clients.insert(client_id, client_info);
774 info!("Client {} registered from {}", client_id, remote_addr);
775 }
776
777 pub fn remove_client(&self, client_id: Uuid) {
779 if self.clients.remove(&client_id).is_some() {
780 info!("Client {} removed", client_id);
781 }
782 }
783
784 pub fn update_client_auth(&self, client_id: Uuid, auth_context: AuthContext) -> bool {
788 self.try_update_client_auth(client_id, auth_context)
789 .unwrap_or(false)
790 }
791
792 #[allow(clippy::result_large_err)]
801 pub fn try_update_client_auth(
802 &self,
803 client_id: Uuid,
804 auth_context: AuthContext,
805 ) -> Result<bool, AuthDeny> {
806 let Some(lifecycle) = self
807 .clients
808 .get(&client_id)
809 .map(|client| client.admission_lifecycle.clone())
810 else {
811 return Ok(false);
812 };
813 let _lifecycle = lifecycle.try_lock().map_err(|_| {
814 AuthDeny::new(
815 AuthErrorCode::InternalError,
816 "Subscription admission is busy",
817 )
818 })?;
819
820 let (subscriptions, current_permit) = {
821 let Some(client) = self.clients.get(&client_id) else {
822 return Ok(false);
823 };
824 if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
825 return Ok(false);
826 }
827 (
828 client.subscriptions.clone(),
829 client.admission_permit.clone(),
830 )
831 };
832 let subscriptions = subscriptions.try_read().map_err(|_| {
833 AuthDeny::new(
834 AuthErrorCode::InternalError,
835 "Subscription admission is busy",
836 )
837 })?;
838 let active_subscriptions = subscriptions.keys().cloned().collect::<Vec<_>>();
839 drop(subscriptions);
840
841 let permit = match &self.admission_provider {
842 Some(provider) => {
843 provider.refresh_connection(current_permit, &auth_context, &active_subscriptions)?
844 }
845 None => current_permit,
846 };
847
848 let Some(mut client) = self.clients.get_mut(&client_id) else {
849 return Ok(false);
850 };
851 if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
852 return Ok(false);
853 }
854 client.admission_permit = permit;
855 client.auth_context = Some(auth_context);
856 debug!("Updated auth context for client {}", client_id);
857 Ok(true)
858 }
859
860 pub async fn update_client_auth_async(
864 &self,
865 client_id: Uuid,
866 auth_context: AuthContext,
867 ) -> bool {
868 self.try_update_client_auth_async(client_id, auth_context)
869 .await
870 .unwrap_or(false)
871 }
872
873 #[allow(clippy::result_large_err)]
874 pub(crate) async fn try_update_client_auth_async(
875 &self,
876 client_id: Uuid,
877 auth_context: AuthContext,
878 ) -> Result<bool, AuthDeny> {
879 let Some(lifecycle) = self
880 .clients
881 .get(&client_id)
882 .map(|client| client.admission_lifecycle.clone())
883 else {
884 return Ok(false);
885 };
886 let _lifecycle = lifecycle.lock().await;
887
888 let (subscriptions, current_permit) = {
889 let Some(client) = self.clients.get(&client_id) else {
890 return Ok(false);
891 };
892 if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
893 return Ok(false);
894 }
895 (
896 client.subscriptions.clone(),
897 client.admission_permit.clone(),
898 )
899 };
900 let subscriptions = subscriptions.read().await;
901 let active_subscriptions = subscriptions.keys().cloned().collect::<Vec<_>>();
902 drop(subscriptions);
903
904 let permit = match &self.admission_provider {
905 Some(provider) => {
906 provider.refresh_connection(current_permit, &auth_context, &active_subscriptions)?
907 }
908 None => current_permit,
909 };
910
911 let Some(mut client) = self.clients.get_mut(&client_id) else {
912 return Ok(false);
913 };
914 if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
915 return Ok(false);
916 }
917 client.admission_permit = permit;
918 client.auth_context = Some(auth_context);
919 debug!("Updated auth context for client {}", client_id);
920 Ok(true)
921 }
922
923 pub fn check_and_remove_expired(&self, client_id: Uuid) -> bool {
928 let now = std::time::SystemTime::now()
929 .duration_since(std::time::UNIX_EPOCH)
930 .unwrap_or_default()
931 .as_secs();
932 let Some((_, client)) = self.clients.remove_if(&client_id, |_, client| {
936 client
937 .auth_context
938 .as_ref()
939 .is_some_and(|ctx| ctx.expires_at <= now)
940 }) else {
941 return false;
942 };
943 if let Some(ctx) = &client.auth_context {
944 warn!(
945 "Client {} token expired (expired at {}), disconnecting",
946 client_id, ctx.expires_at
947 );
948 }
949 let _ = client.close_frame.set(CloseFrame {
953 code: CloseCode::Policy,
954 reason: format!(
955 "{}: Authentication token expired",
956 AuthErrorCode::TokenExpired.as_str()
957 )
958 .into(),
959 });
960 true
961 }
962
963 pub fn client_count(&self) -> usize {
968 self.clients.len()
969 }
970
971 pub fn send_to_client(&self, client_id: Uuid, data: Arc<Bytes>) -> Result<(), SendError> {
980 if self.check_and_remove_expired(client_id) {
982 return Err(SendError::ClientDisconnected);
983 }
984
985 self.enforce_egress_budgets(client_id, data.len())?;
987
988 let sender = {
989 let client = self
990 .clients
991 .get(&client_id)
992 .ok_or(SendError::ClientNotFound)?;
993 client.sender.clone()
994 };
995
996 let msg = Message::Binary((*data).clone());
997 match sender.try_send(msg) {
998 Ok(()) => Ok(()),
999 Err(mpsc::error::TrySendError::Full(_)) => {
1000 warn!(
1001 "Client {} backpressured (queue full), disconnecting",
1002 client_id
1003 );
1004 self.clients.remove(&client_id);
1005 Err(SendError::ClientBackpressured)
1006 }
1007 Err(mpsc::error::TrySendError::Closed(_)) => {
1008 debug!("Client {} channel closed", client_id);
1009 self.clients.remove(&client_id);
1010 Err(SendError::ClientDisconnected)
1011 }
1012 }
1013 }
1014
1015 pub async fn send_to_client_async(
1024 &self,
1025 client_id: Uuid,
1026 data: Arc<Bytes>,
1027 ) -> Result<(), SendError> {
1028 if self.check_and_remove_expired(client_id) {
1030 return Err(SendError::ClientDisconnected);
1031 }
1032
1033 self.enforce_egress_budgets(client_id, data.len())?;
1035
1036 let sender = {
1037 let client = self
1038 .clients
1039 .get(&client_id)
1040 .ok_or(SendError::ClientNotFound)?;
1041 client.sender.clone()
1042 };
1043
1044 let msg = Message::Binary((*data).clone());
1045 sender
1046 .send(msg)
1047 .await
1048 .map_err(|_| SendError::ClientDisconnected)
1049 }
1050
1051 pub async fn send_text_to_client(
1056 &self,
1057 client_id: Uuid,
1058 text: String,
1059 ) -> Result<(), SendError> {
1060 if self.check_and_remove_expired(client_id) {
1062 return Err(SendError::ClientDisconnected);
1063 }
1064
1065 let sender = {
1066 let client = self
1067 .clients
1068 .get(&client_id)
1069 .ok_or(SendError::ClientNotFound)?;
1070 client.sender.clone()
1071 };
1072
1073 let msg = Message::Text(text.into());
1074 sender
1075 .send(msg)
1076 .await
1077 .map_err(|_| SendError::ClientDisconnected)
1078 }
1079
1080 pub async fn send_compressed_async(
1085 &self,
1086 client_id: Uuid,
1087 payload: CompressedPayload,
1088 ) -> Result<(), SendError> {
1089 if self.check_and_remove_expired(client_id) {
1091 return Err(SendError::ClientDisconnected);
1092 }
1093
1094 let (sender, bytes_to_record) = {
1095 let client = self
1096 .clients
1097 .get(&client_id)
1098 .ok_or(SendError::ClientNotFound)?;
1099
1100 let bytes = match &payload {
1101 CompressedPayload::Compressed(bytes) => bytes.len(),
1102 CompressedPayload::Uncompressed(bytes) => bytes.len(),
1103 };
1104
1105 (client.sender.clone(), bytes)
1106 };
1107
1108 self.enforce_egress_budgets(client_id, bytes_to_record)?;
1110
1111 let msg = match payload {
1112 CompressedPayload::Compressed(bytes) => Message::Binary(bytes),
1113 CompressedPayload::Uncompressed(bytes) => Message::Binary(bytes),
1114 };
1115 sender
1116 .send(msg)
1117 .await
1118 .map_err(|_| SendError::ClientDisconnected)
1119 }
1120
1121 pub fn update_client_last_seen(&self, client_id: Uuid) {
1123 if let Some(mut client) = self.clients.get_mut(&client_id) {
1124 client.update_last_seen();
1125 }
1126 }
1127
1128 #[allow(clippy::result_large_err)]
1130 pub fn check_inbound_message_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1131 if self.check_and_remove_expired(client_id) {
1132 return Err(AuthDeny::new(
1133 crate::websocket::auth::AuthErrorCode::TokenExpired,
1134 "Authentication token expired",
1135 ));
1136 }
1137
1138 let (connection_ok, account_ok, permit_id) = {
1139 let Some(client) = self.clients.get(&client_id) else {
1140 return Err(AuthDeny::new(
1141 crate::websocket::auth::AuthErrorCode::InternalError,
1142 "Client not found",
1143 ));
1144 };
1145 let connection_ok = client.record_inbound_message().is_some();
1146 let account_ok = !connection_ok
1147 || client
1148 .auth_context
1149 .as_ref()
1150 .map(|ctx| self.record_account_message(ctx))
1151 .unwrap_or(true);
1152 let permit_id = client.admission_permit.as_ref().map(|permit| permit.id());
1153 (connection_ok, account_ok, permit_id)
1154 };
1155
1156 if connection_ok && account_ok {
1157 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1158 provider.check_inbound_message(permit_id)?;
1159 }
1160 return Ok(());
1161 }
1162 let scope = if connection_ok {
1163 "inbound account websocket messages"
1164 } else {
1165 "inbound websocket messages"
1166 };
1167 Err(
1168 AuthDeny::rate_limited(self.rate_limit_config.message_rate_window, scope).with_context(
1169 format!("client {} exceeded the inbound message budget", client_id),
1170 ),
1171 )
1172 }
1173
1174 pub fn has_client(&self, client_id: Uuid) -> bool {
1176 self.clients.contains_key(&client_id)
1177 }
1178
1179 pub async fn add_client_subscription(
1180 &self,
1181 client_id: Uuid,
1182 subscription_id: String,
1183 token: CancellationToken,
1184 ) -> bool {
1185 self.try_add_client_subscription(client_id, subscription_id, token)
1186 .await
1187 .unwrap_or(false)
1188 }
1189
1190 #[allow(clippy::result_large_err)]
1191 pub(crate) async fn try_add_client_subscription(
1192 &self,
1193 client_id: Uuid,
1194 subscription_id: String,
1195 token: CancellationToken,
1196 ) -> Result<bool, AuthDeny> {
1197 let Some(lifecycle) = self
1198 .clients
1199 .get(&client_id)
1200 .map(|client| client.admission_lifecycle.clone())
1201 else {
1202 return Ok(false);
1203 };
1204 let _lifecycle = lifecycle.lock().await;
1205 let Some((subscriptions, permit)) = self.clients.get(&client_id).and_then(|client| {
1206 Arc::ptr_eq(&client.admission_lifecycle, &lifecycle).then(|| {
1207 (
1208 client.subscriptions.clone(),
1209 client.admission_permit.clone(),
1210 )
1211 })
1212 }) else {
1213 return Ok(false);
1214 };
1215 let permit_id = permit.as_ref().map(|permit| permit.id());
1216 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1217 if !provider.add_subscription(permit_id, &subscription_id)? {
1218 return Ok(false);
1219 }
1220 }
1221 let mut subscriptions = subscriptions.write().await;
1222 let client = self.clients.get(&client_id);
1223 let registered = client
1224 .as_ref()
1225 .is_some_and(|client| Arc::ptr_eq(&client.admission_lifecycle, &lifecycle));
1226 if !registered || subscriptions.contains_key(&subscription_id) {
1227 drop(client);
1228 drop(subscriptions);
1229 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1230 provider.remove_subscription(permit_id, &subscription_id);
1231 }
1232 return Ok(false);
1233 }
1234 subscriptions.insert(subscription_id, token);
1235 drop(client);
1236 Ok(true)
1237 }
1238
1239 pub async fn remove_client_subscription(&self, client_id: Uuid, subscription_id: &str) -> bool {
1240 let Some(lifecycle) = self
1241 .clients
1242 .get(&client_id)
1243 .map(|client| client.admission_lifecycle.clone())
1244 else {
1245 return false;
1246 };
1247 let _lifecycle = lifecycle.lock().await;
1248 let Some((subscriptions, permit)) = self.clients.get(&client_id).and_then(|client| {
1249 Arc::ptr_eq(&client.admission_lifecycle, &lifecycle).then(|| {
1250 (
1251 client.subscriptions.clone(),
1252 client.admission_permit.clone(),
1253 )
1254 })
1255 }) else {
1256 return false;
1257 };
1258 let permit_id = permit.as_ref().map(|permit| permit.id());
1259 let mut subscriptions = subscriptions.write().await;
1260 let client = self.clients.get(&client_id);
1261 if !client
1262 .as_ref()
1263 .is_some_and(|client| Arc::ptr_eq(&client.admission_lifecycle, &lifecycle))
1264 {
1265 return false;
1266 }
1267 let token = subscriptions.remove(subscription_id);
1268 drop(client);
1269 drop(subscriptions);
1270 if let Some(token) = token {
1271 token.cancel();
1272 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1273 provider.remove_subscription(permit_id, subscription_id);
1274 }
1275 true
1276 } else {
1277 false
1278 }
1279 }
1280
1281 pub async fn replace_client_subscription_token(
1286 &self,
1287 client_id: Uuid,
1288 subscription_id: &str,
1289 cancel_token: CancellationToken,
1290 ) -> bool {
1291 let Some(lifecycle) = self
1292 .clients
1293 .get(&client_id)
1294 .map(|client| client.admission_lifecycle.clone())
1295 else {
1296 return false;
1297 };
1298 let _lifecycle = lifecycle.lock().await;
1299 let Some(subscriptions) = self.clients.get(&client_id).and_then(|client| {
1300 Arc::ptr_eq(&client.admission_lifecycle, &lifecycle)
1301 .then(|| client.subscriptions.clone())
1302 }) else {
1303 return false;
1304 };
1305 let mut subscriptions = subscriptions.write().await;
1306 match subscriptions.get_mut(subscription_id) {
1307 Some(token) => {
1308 std::mem::replace(token, cancel_token).cancel();
1309 true
1310 }
1311 None => false,
1312 }
1313 }
1314
1315 pub async fn cancel_all_client_subscriptions(&self, client_id: Uuid) {
1316 let Some(lifecycle) = self
1317 .clients
1318 .get(&client_id)
1319 .map(|client| client.admission_lifecycle.clone())
1320 else {
1321 return;
1322 };
1323 let _lifecycle = lifecycle.lock().await;
1324 let Some(subscriptions) = self.clients.get(&client_id).and_then(|client| {
1325 Arc::ptr_eq(&client.admission_lifecycle, &lifecycle)
1326 .then(|| client.subscriptions.clone())
1327 }) else {
1328 return;
1329 };
1330 let subscriptions = subscriptions.read().await;
1331 for (subscription_id, token) in subscriptions.iter() {
1332 token.cancel();
1333 debug!("Cancelled subscription on disconnect: {}", subscription_id);
1334 }
1335 }
1336
1337 pub fn cleanup_stale_clients(&self) -> usize {
1339 let timeout = self.rate_limit_config.client_timeout;
1340 let mut stale_clients = Vec::new();
1341
1342 for entry in self.clients.iter() {
1343 if entry.value().is_stale(timeout) {
1344 stale_clients.push(*entry.key());
1345 }
1346 }
1347
1348 let removed_count = stale_clients.len();
1349 for client_id in stale_clients {
1350 self.clients.remove(&client_id);
1351 info!("Removed stale client {}", client_id);
1352 }
1353
1354 removed_count
1355 }
1356
1357 fn cleanup_account_state(&self) {
1360 let live_accounts: HashSet<String> = self
1361 .clients
1362 .iter()
1363 .filter_map(|entry| {
1364 entry
1365 .value()
1366 .auth_context
1367 .as_ref()
1368 .and_then(|ctx| ctx.account_key.clone())
1369 })
1370 .collect();
1371 self.account_usage.retain(|account, usage| {
1372 live_accounts.contains(account) || usage.idle_for() < ACCOUNT_USAGE_IDLE_TTL
1373 });
1374 self.account_policies
1375 .evict_idle(|account| live_accounts.contains(account));
1376 }
1377
1378 pub fn start_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
1383 let client_manager = self.clone();
1384
1385 tokio::spawn(async move {
1386 let mut interval = tokio::time::interval(Duration::from_secs(30));
1387
1388 loop {
1389 interval.tick().await;
1390 let removed = client_manager.cleanup_stale_clients();
1391 if removed > 0 {
1392 info!("Cleaned up {} stale clients", removed);
1393 }
1394 client_manager.cleanup_account_state();
1395 if let Some(provider) = &client_manager.admission_provider {
1396 provider.cleanup();
1397 }
1398 }
1399 })
1400 }
1401
1402 #[allow(clippy::result_large_err)]
1410 pub async fn check_connection_allowed(
1411 &self,
1412 remote_addr: SocketAddr,
1413 auth_context: &Option<AuthContext>,
1414 ) -> Result<(), AuthDeny> {
1415 if let Some(ref rate_limiter) = self.rate_limiter {
1417 match rate_limiter.check_handshake(remote_addr).await {
1419 RateLimitResult::Allowed { .. } => {}
1420 RateLimitResult::Denied { retry_after, limit } => {
1421 return Err(AuthDeny::rate_limited(retry_after, "websocket handshakes")
1422 .with_context(format!(
1423 "handshake rate limit of {} per minute exceeded for {}",
1424 limit, remote_addr
1425 )));
1426 }
1427 }
1428
1429 if let Some(ref ctx) = auth_context {
1430 match rate_limiter
1434 .check_connection_for_consumer(
1435 ctx.consumer_key(),
1436 ctx.limits.max_connection_attempts_per_minute,
1437 )
1438 .await
1439 {
1440 RateLimitResult::Allowed { .. } => {}
1441 RateLimitResult::Denied { retry_after, limit } => {
1442 return Err(AuthDeny::rate_limited(retry_after, "websocket connections")
1443 .with_context(format!(
1444 "connection rate limit for consumer {} of {} per minute exceeded",
1445 ctx.consumer_key(),
1446 limit
1447 )));
1448 }
1449 }
1450
1451 if ctx.is_legacy_policy() || ctx.account_key.is_some() {
1455 match rate_limiter
1456 .check_connection_for_account(
1457 ctx.account_key(),
1458 ctx.account_limits.max_connection_attempts_per_minute,
1459 )
1460 .await
1461 {
1462 RateLimitResult::Allowed { .. } => {}
1463 RateLimitResult::Denied { retry_after, limit } => {
1464 return Err(AuthDeny::rate_limited(
1465 retry_after,
1466 "metered websocket connections",
1467 )
1468 .with_context(format!(
1469 "connection rate limit for account {} of {} per minute exceeded",
1470 ctx.account_key(),
1471 limit
1472 )));
1473 }
1474 }
1475 }
1476 }
1477 }
1478
1479 if let Some(max_per_ip) = self.rate_limit_config.max_connections_per_ip {
1481 let current_ip_connections = self.count_connections_for_ip(&remote_addr);
1482 if current_ip_connections >= max_per_ip {
1483 return Err(AuthDeny::connection_limit_exceeded(
1484 &format!("ip {}", remote_addr.ip()),
1485 current_ip_connections,
1486 max_per_ip,
1487 ));
1488 }
1489 }
1490
1491 if let Some(ctx) = auth_context {
1492 if ctx.is_legacy_policy() {
1495 let legacy_policy_token = self.account_policies.record_legacy_token();
1496 debug!(legacy_policy_token, "legacy policy token admitted");
1497 } else if let (Some(account), Some(policy_version)) =
1498 (ctx.account_key.as_deref(), ctx.policy_version)
1499 {
1500 match self
1501 .account_policies
1502 .observe(account, policy_version, &ctx.account_limits)
1503 {
1504 Ok(()) => {}
1505 Err(AccountPolicyError::StaleVersion { presented, current }) => {
1506 debug!(
1507 account = %redact_identity(account),
1508 presented,
1509 current,
1510 "stale policy version rejected"
1511 );
1512 return Err(AuthDeny::new(
1513 crate::websocket::auth::AuthErrorCode::TokenExpired,
1514 "session policy version is stale; refresh the session token",
1515 ));
1516 }
1517 Err(AccountPolicyError::ConflictingLimits { version }) => {
1518 warn!(
1519 account = %redact_identity(account),
1520 version,
1521 "signed account limits conflict for one policy version"
1522 );
1523 return Err(AuthDeny::new(
1524 crate::websocket::auth::AuthErrorCode::InternalError,
1525 "signed account limits conflict with previously observed policy",
1526 ));
1527 }
1528 Err(AccountPolicyError::CapacityExhausted) => {
1529 warn!("account policy state at capacity; denying admission");
1530 return Err(AuthDeny::new(
1531 crate::websocket::auth::AuthErrorCode::InternalError,
1532 "account policy state is at capacity; retry shortly",
1533 ));
1534 }
1535 }
1536 }
1537
1538 let max_connections = ctx.limits.max_connections.or_else(|| {
1542 self.rate_limit_config
1543 .default_limits
1544 .as_ref()
1545 .and_then(|l| l.max_connections)
1546 });
1547 if let Some(max_connections) = max_connections {
1548 let current_connections = self.count_connections_for_consumer(ctx.consumer_key());
1549 if current_connections >= max_connections as usize {
1550 return Err(AuthDeny::connection_limit_exceeded(
1551 &format!("consumer {}", ctx.consumer_key()),
1552 current_connections,
1553 max_connections as usize,
1554 ));
1555 }
1556 }
1557
1558 if !ctx.is_legacy_policy() && ctx.account_key.is_some() {
1561 if let Some(max_account_connections) = ctx.account_limits.max_connections {
1562 let current_account_connections =
1563 self.count_connections_for_account(ctx.account_key());
1564 if current_account_connections >= max_account_connections as usize {
1565 return Err(AuthDeny::connection_limit_exceeded(
1566 &format!("account {}", ctx.account_key()),
1567 current_account_connections,
1568 max_account_connections as usize,
1569 ));
1570 }
1571 }
1572 }
1573
1574 if let Some(max_per_metering_key) =
1576 self.rate_limit_config.max_connections_per_metering_key
1577 {
1578 let current_metering_connections =
1579 self.count_connections_for_metering_key(&ctx.metering_key);
1580 if current_metering_connections >= max_per_metering_key {
1581 return Err(AuthDeny::connection_limit_exceeded(
1582 &format!("metering key {}", ctx.metering_key),
1583 current_metering_connections,
1584 max_per_metering_key,
1585 ));
1586 }
1587 }
1588
1589 if let Some(max_per_origin) = self.rate_limit_config.max_connections_per_origin {
1591 if let Some(ref origin) = ctx.origin {
1592 let current_origin_connections = self.count_connections_for_origin(origin);
1593 if current_origin_connections >= max_per_origin {
1594 return Err(AuthDeny::connection_limit_exceeded(
1595 &format!("origin {}", origin),
1596 current_origin_connections,
1597 max_per_origin,
1598 ));
1599 }
1600 }
1601 }
1602 }
1603 Ok(())
1604 }
1605
1606 fn count_connections_for_ip(&self, remote_addr: &SocketAddr) -> usize {
1608 let ip = remote_addr.ip();
1609 self.clients
1610 .iter()
1611 .filter(|entry| entry.value().remote_addr.ip() == ip)
1612 .count()
1613 }
1614
1615 fn count_connections_for_consumer(&self, consumer: &str) -> usize {
1617 self.clients
1618 .iter()
1619 .filter(|entry| {
1620 entry
1621 .value()
1622 .auth_context
1623 .as_ref()
1624 .map(|ctx| ctx.consumer_key() == consumer)
1625 .unwrap_or(false)
1626 })
1627 .count()
1628 }
1629
1630 fn count_connections_for_account(&self, account: &str) -> usize {
1632 self.clients
1633 .iter()
1634 .filter(|entry| {
1635 entry
1636 .value()
1637 .auth_context
1638 .as_ref()
1639 .map(|ctx| ctx.account_key() == account)
1640 .unwrap_or(false)
1641 })
1642 .count()
1643 }
1644
1645 fn count_connections_for_metering_key(&self, metering_key: &str) -> usize {
1647 self.clients
1648 .iter()
1649 .filter(|entry| {
1650 entry
1651 .value()
1652 .auth_context
1653 .as_ref()
1654 .map(|ctx| ctx.metering_key == metering_key)
1655 .unwrap_or(false)
1656 })
1657 .count()
1658 }
1659
1660 fn count_connections_for_origin(&self, origin: &str) -> usize {
1662 self.clients
1663 .iter()
1664 .filter(|entry| {
1665 entry
1666 .value()
1667 .auth_context
1668 .as_ref()
1669 .and_then(|ctx| ctx.origin.as_ref())
1670 .map(|o| o == origin)
1671 .unwrap_or(false)
1672 })
1673 .count()
1674 }
1675
1676 #[allow(clippy::result_large_err)]
1680 pub async fn check_subscription_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1681 let context = {
1682 let Some(client) = self.clients.get(&client_id) else {
1683 return Ok(());
1684 };
1685 let current_subs = client.subscription_count().await;
1686
1687 if let Some(ref ctx) = client.auth_context {
1689 let max_subs = ctx.limits.max_subscriptions.or_else(|| {
1690 self.rate_limit_config
1691 .default_limits
1692 .as_ref()
1693 .and_then(|l| l.max_subscriptions)
1694 });
1695 if let Some(max_subs) = max_subs {
1696 if current_subs >= max_subs as usize {
1697 return Err(AuthDeny::new(
1698 crate::websocket::auth::AuthErrorCode::SubscriptionLimitExceeded,
1699 format!(
1700 "Subscription limit exceeded: {} of {} subscriptions for client {}",
1701 current_subs, max_subs, client_id
1702 ),
1703 )
1704 .with_suggested_action(
1705 "Unsubscribe from an existing view before creating another subscription",
1706 ));
1707 }
1708 }
1709 }
1710 client.auth_context.clone()
1711 };
1712
1713 if let (Some(rate_limiter), Some(ctx)) = (self.rate_limiter.as_ref(), context.as_ref()) {
1716 if !ctx.is_legacy_policy() {
1717 if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1718 .check_subscription_create_for_consumer(
1719 ctx.consumer_key(),
1720 ctx.limits.max_subscription_creates_per_minute,
1721 )
1722 .await
1723 {
1724 return Err(AuthDeny::rate_limited(retry_after, "subscription creates")
1725 .with_context(format!(
1726 "subscription-create rate limit for consumer {} of {} per minute exceeded",
1727 ctx.consumer_key(),
1728 limit
1729 )));
1730 }
1731
1732 if ctx.account_key.is_some() {
1733 if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1734 .check_subscription_create_for_account(
1735 ctx.account_key(),
1736 ctx.account_limits.max_subscription_creates_per_minute,
1737 )
1738 .await
1739 {
1740 return Err(AuthDeny::rate_limited(
1741 retry_after,
1742 "account subscription creates",
1743 )
1744 .with_context(format!(
1745 "subscription-create rate limit for account {} of {} per minute exceeded",
1746 ctx.account_key(),
1747 limit
1748 )));
1749 }
1750 }
1751 }
1752 }
1753 Ok(())
1754 }
1755
1756 pub fn get_metering_key(&self, client_id: Uuid) -> Option<String> {
1758 self.clients.get(&client_id).and_then(|client| {
1759 client
1760 .auth_context
1761 .as_ref()
1762 .map(|ctx| ctx.metering_key.clone())
1763 })
1764 }
1765
1766 pub fn get_auth_context(&self, client_id: Uuid) -> Option<AuthContext> {
1768 self.clients
1769 .get(&client_id)
1770 .and_then(|client| client.auth_context.clone())
1771 }
1772
1773 #[allow(clippy::result_large_err)]
1777 pub fn check_snapshot_allowed(
1778 &self,
1779 client_id: Uuid,
1780 requested_rows: u32,
1781 ) -> Result<(), AuthDeny> {
1782 if let Some(client) = self.clients.get(&client_id) {
1783 if let Some(ref ctx) = client.auth_context {
1784 let max_rows = ctx.limits.max_snapshot_rows.or_else(|| {
1785 self.rate_limit_config
1786 .default_limits
1787 .as_ref()
1788 .and_then(|l| l.max_snapshot_rows)
1789 });
1790 if let Some(max_rows) = max_rows {
1791 if requested_rows > max_rows {
1792 return Err(AuthDeny::new(
1793 crate::websocket::auth::AuthErrorCode::SnapshotLimitExceeded,
1794 format!(
1795 "Snapshot limit exceeded: requested {} rows, max allowed is {} for client {}",
1796 requested_rows, max_rows, client_id
1797 ),
1798 )
1799 .with_suggested_action(
1800 "Request fewer rows or lower the snapshotLimit on the subscription",
1801 ));
1802 }
1803 }
1804 }
1805 }
1806 Ok(())
1807 }
1808}
1809
1810impl Default for ClientManager {
1811 fn default() -> Self {
1812 Self::new()
1813 }
1814}
1815
1816#[cfg(test)]
1817#[path = "admission_tests.rs"]
1818mod admission_tests;
1819
1820#[cfg(test)]
1821mod tests {
1822 use super::*;
1823 use crate::websocket::auth::AuthContext;
1824 use arete_auth::{KeyClass, Limits};
1825 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1826
1827 fn create_test_auth_context(subject: &str, limits: Limits) -> AuthContext {
1828 AuthContext {
1829 subject: subject.to_string(),
1830 issuer: "test-issuer".to_string(),
1831 audience: "test-audience".to_string(),
1832 key_class: KeyClass::Publishable,
1833 metering_key: format!("meter-{}", subject),
1834 deployment_id: None,
1835 target_kind: None,
1836 target_id: None,
1837 program_id: None,
1838 program_release_hash: None,
1839 expires_at: u64::MAX,
1840 scope: "read".to_string(),
1841 limits,
1842 plan: None,
1843 origin: None,
1844 client_ip: None,
1845 jti: uuid::Uuid::new_v4().to_string(),
1846 actor_key: None,
1847 account_key: None,
1848 consumer_key: None,
1849 policy_version: None,
1850 account_limits: Limits::default(),
1851 row_scopes: None,
1852 }
1853 }
1854
1855 fn create_v2_auth_context(
1856 consumer: &str,
1857 account: &str,
1858 policy_version: u32,
1859 limits: Limits,
1860 account_limits: Limits,
1861 ) -> AuthContext {
1862 AuthContext {
1863 subject: "user:1".to_string(),
1864 issuer: "test-issuer".to_string(),
1865 audience: "test-audience".to_string(),
1866 key_class: KeyClass::Publishable,
1867 metering_key: account.to_string(),
1868 deployment_id: None,
1869 target_kind: None,
1870 target_id: None,
1871 program_id: None,
1872 program_release_hash: None,
1873 expires_at: u64::MAX,
1874 scope: "read".to_string(),
1875 limits,
1876 plan: Some("pro".to_string()),
1877 origin: None,
1878 client_ip: None,
1879 jti: uuid::Uuid::new_v4().to_string(),
1880 actor_key: Some("user:1".to_string()),
1881 account_key: Some(account.to_string()),
1882 consumer_key: Some(consumer.to_string()),
1883 policy_version: Some(policy_version),
1884 account_limits,
1885 row_scopes: None,
1886 }
1887 }
1888
1889 fn insert_client(manager: &ClientManager, context: AuthContext) -> Uuid {
1890 let (sender, receiver) = mpsc::channel(8);
1891 std::mem::forget(receiver);
1893 let client_id = Uuid::new_v4();
1894 manager.clients.insert(
1895 client_id,
1896 ClientInfo::new(
1897 client_id,
1898 sender,
1899 Some(context),
1900 create_test_socket_addr("127.0.0.1"),
1901 ),
1902 );
1903 client_id
1904 }
1905
1906 fn create_test_socket_addr(ip: &str) -> SocketAddr {
1907 SocketAddr::new(
1908 ip.parse::<IpAddr>()
1909 .unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1910 12345,
1911 )
1912 }
1913
1914 #[test]
1915 fn test_egress_tracker_basic() {
1916 let mut tracker = EgressTracker::new();
1917
1918 assert!(tracker.record_bytes(500, 1000));
1920 assert_eq!(tracker.current_usage(), 500);
1921
1922 assert!(tracker.record_bytes(400, 1000));
1924 assert_eq!(tracker.current_usage(), 900);
1925
1926 assert!(!tracker.record_bytes(200, 1000));
1928 assert_eq!(tracker.current_usage(), 900); }
1930
1931 #[test]
1932 fn test_egress_tracker_window_reset() {
1933 let mut tracker = EgressTracker::new();
1934
1935 assert!(tracker.record_bytes(100, 100));
1937 assert!(!tracker.record_bytes(1, 100));
1938
1939 tracker.bytes_this_minute = 0;
1941 tracker.window_start = SystemTime::now() - Duration::from_secs(61);
1942
1943 assert!(tracker.record_bytes(50, 100));
1945 }
1946
1947 #[test]
1948 fn test_message_rate_tracker_basic() {
1949 let mut tracker = MessageRateTracker::new();
1950
1951 assert!(tracker.record_message(2));
1952 assert_eq!(tracker.current_usage(), 1);
1953
1954 assert!(tracker.record_message(2));
1955 assert_eq!(tracker.current_usage(), 2);
1956
1957 assert!(!tracker.record_message(2));
1958 assert_eq!(tracker.current_usage(), 2);
1959 }
1960
1961 #[tokio::test]
1962 async fn test_client_inbound_message_limit() {
1963 let (tx, _rx) = mpsc::channel(1);
1964 let client = ClientInfo::new(
1965 Uuid::new_v4(),
1966 tx,
1967 Some(create_test_auth_context(
1968 "user-1",
1969 Limits {
1970 max_messages_per_minute: Some(2),
1971 ..Default::default()
1972 },
1973 )),
1974 create_test_socket_addr("127.0.0.1"),
1975 );
1976
1977 assert_eq!(client.record_inbound_message(), Some(1));
1978 assert_eq!(client.record_inbound_message(), Some(2));
1979 assert_eq!(client.record_inbound_message(), None);
1980 }
1981
1982 #[tokio::test]
1983 async fn duplicate_subscription_id_is_rejected_without_replacement() {
1984 let (tx, _rx) = mpsc::channel(1);
1985 let client = ClientInfo::new(
1986 Uuid::new_v4(),
1987 tx,
1988 None,
1989 create_test_socket_addr("127.0.0.1"),
1990 );
1991 let first = CancellationToken::new();
1992 let duplicate = CancellationToken::new();
1993
1994 assert!(
1995 client
1996 .add_subscription("opaque-id".to_string(), first.clone())
1997 .await
1998 );
1999 assert!(
2000 !client
2001 .add_subscription("opaque-id".to_string(), duplicate.clone())
2002 .await
2003 );
2004 assert!(!first.is_cancelled());
2005 assert!(!duplicate.is_cancelled());
2006
2007 assert!(client.remove_subscription("opaque-id").await);
2008 assert!(first.is_cancelled());
2009 assert!(!duplicate.is_cancelled());
2010 }
2011
2012 #[tokio::test]
2013 async fn test_no_limits() {
2014 let manager = ClientManager::new();
2015 let addr = create_test_socket_addr("127.0.0.1");
2016
2017 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
2019
2020 let auth_context = create_test_auth_context("test", Limits::default());
2022 assert!(manager
2023 .check_connection_allowed(addr, &Some(auth_context))
2024 .await
2025 .is_ok());
2026 }
2027
2028 #[tokio::test]
2029 async fn test_per_subject_connection_limit() {
2030 let manager = ClientManager::new();
2031
2032 let limits = Limits {
2033 max_connections: Some(2),
2034 ..Default::default()
2035 };
2036
2037 let auth_context = create_test_auth_context("user-1", limits);
2038 let addr = create_test_socket_addr("127.0.0.1");
2039
2040 assert!(manager
2042 .check_connection_allowed(addr, &Some(auth_context.clone()))
2043 .await
2044 .is_ok());
2045 }
2046
2047 #[tokio::test]
2048 async fn test_per_ip_connection_limit() {
2049 let manager = ClientManager::new().with_max_connections_per_ip(2);
2050 let addr = create_test_socket_addr("192.168.1.1");
2051
2052 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
2054 }
2055
2056 #[test]
2058 fn rate_limit_config_default() {
2059 let config = RateLimitConfig::default();
2060 assert!(config.max_connections_per_ip.is_none());
2061 assert_eq!(config.client_timeout, Duration::from_secs(300));
2062 assert_eq!(config.message_queue_size, 512);
2063 assert!(config.max_reconnect_attempts.is_none());
2064 assert_eq!(config.message_rate_window, Duration::from_secs(60));
2065 assert_eq!(config.egress_rate_window, Duration::from_secs(60));
2066 }
2067
2068 #[test]
2069 fn rate_limit_config_builder_methods() {
2070 let config = RateLimitConfig::default()
2071 .with_max_connections_per_ip(10)
2072 .with_timeout(Duration::from_secs(600))
2073 .with_message_queue_size(1024)
2074 .with_rate_limit_window(Duration::from_secs(120));
2075
2076 assert_eq!(config.max_connections_per_ip, Some(10));
2077 assert_eq!(config.client_timeout, Duration::from_secs(600));
2078 assert_eq!(config.message_queue_size, 1024);
2079 assert_eq!(config.message_rate_window, Duration::from_secs(120));
2080 assert_eq!(config.egress_rate_window, Duration::from_secs(120));
2081 }
2082
2083 #[tokio::test]
2084 async fn client_manager_with_config() {
2085 let config = RateLimitConfig::default()
2086 .with_max_connections_per_ip(5)
2087 .with_timeout(Duration::from_secs(120))
2088 .with_message_queue_size(256);
2089
2090 let manager = ClientManager::with_config(config);
2091 let addr = create_test_socket_addr("10.0.0.1");
2092
2093 assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(5));
2095 assert_eq!(
2096 manager.rate_limit_config().client_timeout,
2097 Duration::from_secs(120)
2098 );
2099 assert_eq!(manager.rate_limit_config().message_queue_size, 256);
2100
2101 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
2103 }
2104
2105 #[tokio::test]
2106 async fn client_manager_builder_pattern() {
2107 let manager = ClientManager::new()
2108 .with_max_connections_per_ip(10)
2109 .with_timeout(Duration::from_secs(180))
2110 .with_message_queue_size(1024)
2111 .with_rate_limit_window(Duration::from_secs(90));
2112
2113 assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(10));
2114 assert_eq!(
2115 manager.rate_limit_config().client_timeout,
2116 Duration::from_secs(180)
2117 );
2118 assert_eq!(manager.rate_limit_config().message_queue_size, 1024);
2119 assert_eq!(
2120 manager.rate_limit_config().message_rate_window,
2121 Duration::from_secs(90)
2122 );
2123 }
2124
2125 #[tokio::test]
2127 async fn connection_limit_enforcement_with_actual_clients() {
2128 let manager = ClientManager::new().with_max_connections_per_ip(2);
2129 let addr1 = create_test_socket_addr("192.168.1.1");
2130 let addr2 = create_test_socket_addr("192.168.1.2");
2131
2132 let auth1 = create_test_auth_context("user-1", Limits::default());
2134 assert!(manager
2135 .check_connection_allowed(addr1, &Some(auth1.clone()))
2136 .await
2137 .is_ok());
2138
2139 let auth2 = create_test_auth_context("user-2", Limits::default());
2144 assert!(manager
2145 .check_connection_allowed(addr1, &Some(auth2.clone()))
2146 .await
2147 .is_ok());
2148
2149 let auth3 = create_test_auth_context("user-3", Limits::default());
2151 assert!(manager
2152 .check_connection_allowed(addr2, &Some(auth3.clone()))
2153 .await
2154 .is_ok());
2155 }
2156
2157 #[tokio::test]
2159 async fn subscription_limit_enforcement() {
2160 let manager = ClientManager::new();
2161 let addr = create_test_socket_addr("127.0.0.1");
2162
2163 let auth = create_test_auth_context(
2165 "user-1",
2166 Limits {
2167 max_subscriptions: Some(2),
2168 ..Default::default()
2169 },
2170 );
2171
2172 assert!(manager
2174 .check_connection_allowed(addr, &Some(auth.clone()))
2175 .await
2176 .is_ok());
2177
2178 assert_eq!(auth.limits.max_subscriptions, Some(2));
2181 }
2182
2183 #[tokio::test]
2185 async fn snapshot_limit_enforcement() {
2186 let manager = ClientManager::new();
2187 let addr = create_test_socket_addr("127.0.0.1");
2188
2189 let auth = create_test_auth_context(
2190 "user-1",
2191 Limits {
2192 max_snapshot_rows: Some(1000),
2193 ..Default::default()
2194 },
2195 );
2196
2197 assert!(manager
2198 .check_connection_allowed(addr, &Some(auth.clone()))
2199 .await
2200 .is_ok());
2201
2202 }
2205
2206 #[tokio::test]
2207 async fn two_consumers_share_the_account_connection_cap() {
2208 let manager = ClientManager::new();
2209 let account_limits = Limits {
2210 max_connections: Some(1),
2211 ..Limits::default()
2212 };
2213 let consumer_a = create_v2_auth_context(
2214 "consumer:a",
2215 "account:42",
2216 1,
2217 Limits::default(),
2218 account_limits.clone(),
2219 );
2220 insert_client(&manager, consumer_a);
2221
2222 let consumer_b = create_v2_auth_context(
2225 "consumer:b",
2226 "account:42",
2227 1,
2228 Limits::default(),
2229 account_limits.clone(),
2230 );
2231 let addr = create_test_socket_addr("127.0.0.1");
2232 assert!(manager
2233 .check_connection_allowed(addr, &Some(consumer_b))
2234 .await
2235 .is_err());
2236
2237 let other_account = create_v2_auth_context(
2239 "consumer:c",
2240 "account:43",
2241 1,
2242 Limits::default(),
2243 account_limits,
2244 );
2245 assert!(manager
2246 .check_connection_allowed(addr, &Some(other_account))
2247 .await
2248 .is_ok());
2249 }
2250
2251 #[tokio::test]
2252 async fn one_consumer_is_limited_independently_of_its_account() {
2253 let manager = ClientManager::new();
2254 let limits = Limits {
2255 max_connections: Some(1),
2256 ..Limits::default()
2257 };
2258 let account_limits = Limits {
2259 max_connections: Some(10),
2260 ..Limits::default()
2261 };
2262 let consumer_a = create_v2_auth_context(
2263 "consumer:a",
2264 "account:42",
2265 1,
2266 limits.clone(),
2267 account_limits.clone(),
2268 );
2269 insert_client(&manager, consumer_a.clone());
2270
2271 let addr = create_test_socket_addr("127.0.0.1");
2272 assert!(manager
2274 .check_connection_allowed(addr, &Some(consumer_a))
2275 .await
2276 .is_err());
2277
2278 let consumer_b =
2280 create_v2_auth_context("consumer:b", "account:42", 1, limits, account_limits);
2281 assert!(manager
2282 .check_connection_allowed(addr, &Some(consumer_b))
2283 .await
2284 .is_ok());
2285 }
2286
2287 #[tokio::test]
2288 async fn policy_version_upgrade_stale_and_conflict_rules_apply() {
2289 let manager = ClientManager::new();
2290 let addr = create_test_socket_addr("127.0.0.1");
2291 let limits_v1 = Limits {
2292 max_connections: Some(5),
2293 ..Limits::default()
2294 };
2295 let limits_v2 = Limits {
2296 max_connections: Some(2),
2297 ..Limits::default()
2298 };
2299
2300 let v1 = create_v2_auth_context(
2302 "consumer:a",
2303 "account:42",
2304 1,
2305 Limits::default(),
2306 limits_v1.clone(),
2307 );
2308 assert!(manager
2309 .check_connection_allowed(addr, &Some(v1.clone()))
2310 .await
2311 .is_ok());
2312
2313 let v2 = create_v2_auth_context(
2315 "consumer:a",
2316 "account:42",
2317 2,
2318 Limits::default(),
2319 limits_v2.clone(),
2320 );
2321 assert!(manager
2322 .check_connection_allowed(addr, &Some(v2))
2323 .await
2324 .is_ok());
2325
2326 let stale = manager.check_connection_allowed(addr, &Some(v1)).await;
2328 assert!(stale.is_err());
2329
2330 let conflict =
2332 create_v2_auth_context("consumer:a", "account:42", 2, Limits::default(), limits_v1);
2333 assert!(manager
2334 .check_connection_allowed(addr, &Some(conflict))
2335 .await
2336 .is_err());
2337 }
2338
2339 #[tokio::test]
2340 async fn legacy_tokens_keep_old_behavior_and_are_counted() {
2341 let manager = ClientManager::new();
2342 let addr = create_test_socket_addr("127.0.0.1");
2343 let legacy = create_test_auth_context(
2344 "user-1",
2345 Limits {
2346 max_connections: Some(2),
2347 ..Limits::default()
2348 },
2349 );
2350 assert!(legacy.is_legacy_policy());
2351
2352 assert!(manager
2353 .check_connection_allowed(addr, &Some(legacy.clone()))
2354 .await
2355 .is_ok());
2356 assert_eq!(manager.account_policies().legacy_token_count(), 1);
2357 assert_eq!(manager.account_policies().tracked_accounts(), 0);
2359
2360 insert_client(&manager, legacy.clone());
2362 insert_client(&manager, legacy.clone());
2363 assert!(manager
2364 .check_connection_allowed(addr, &Some(legacy))
2365 .await
2366 .is_err());
2367 }
2368
2369 #[tokio::test]
2370 async fn account_message_budget_is_shared_across_consumers() {
2371 let manager = ClientManager::new();
2372 let account_limits = Limits {
2373 max_messages_per_minute: Some(2),
2374 ..Limits::default()
2375 };
2376 let client_a = insert_client(
2377 &manager,
2378 create_v2_auth_context(
2379 "consumer:a",
2380 "account:42",
2381 1,
2382 Limits::default(),
2383 account_limits.clone(),
2384 ),
2385 );
2386 let client_b = insert_client(
2387 &manager,
2388 create_v2_auth_context(
2389 "consumer:b",
2390 "account:42",
2391 1,
2392 Limits::default(),
2393 account_limits,
2394 ),
2395 );
2396
2397 assert!(manager.check_inbound_message_allowed(client_a).is_ok());
2398 assert!(manager.check_inbound_message_allowed(client_b).is_ok());
2399 assert!(manager.check_inbound_message_allowed(client_a).is_err());
2401 }
2402
2403 #[tokio::test]
2404 async fn account_state_evicts_when_idle_and_unreferenced() {
2405 let manager = ClientManager::new();
2406 let addr = create_test_socket_addr("127.0.0.1");
2407 let context = create_v2_auth_context(
2408 "consumer:a",
2409 "account:42",
2410 1,
2411 Limits::default(),
2412 Limits::default(),
2413 );
2414 assert!(manager
2415 .check_connection_allowed(addr, &Some(context))
2416 .await
2417 .is_ok());
2418 assert_eq!(manager.account_policies().tracked_accounts(), 1);
2419
2420 manager.account_policies().evict_idle(|_| false);
2423 assert_eq!(
2424 manager.account_policies().tracked_accounts(),
2425 1,
2426 "TTL keeps fresh entries"
2427 );
2428 manager.cleanup_account_state();
2429 assert_eq!(
2430 manager.account_policies().tracked_accounts(),
2431 1,
2432 "fresh entries survive sweep"
2433 );
2434 }
2435
2436 #[test]
2437 fn expired_token_is_removed_without_deadlocking() {
2438 let manager = ClientManager::new();
2439 let mut context = create_test_auth_context("user-1", Limits::default());
2440 context.expires_at = 1;
2441 let client_id = insert_client(&manager, context);
2442
2443 let (done_tx, done_rx) = std::sync::mpsc::channel();
2446 let sender = manager.clone();
2447 std::thread::spawn(move || {
2448 let result = sender.send_to_client(client_id, Arc::new(Bytes::from_static(b"update")));
2449 let _ = done_tx.send(result);
2450 });
2451
2452 let result = done_rx
2453 .recv_timeout(Duration::from_secs(5))
2454 .expect("send_to_client deadlocked on an expired token");
2455 assert_eq!(result, Err(SendError::ClientDisconnected));
2456 assert!(!manager.has_client(client_id));
2457 }
2458
2459 #[tokio::test]
2460 async fn an_expired_client_keeps_its_close_reason_when_its_queue_is_full() {
2461 let manager = ClientManager::new();
2462 let (sender, mut queue) = mpsc::channel(1);
2463 sender
2464 .try_send(Message::Text("backlog".into()))
2465 .expect("room for one message");
2466 let client_id = Uuid::new_v4();
2467 let mut context = create_test_auth_context("user-1", Limits::default());
2468 context.expires_at = 1;
2469 let client = ClientInfo::new(
2470 client_id,
2471 sender,
2472 Some(context),
2473 create_test_socket_addr("127.0.0.1"),
2474 );
2475 let close_frame = client.close_frame.clone();
2476 manager.clients.insert(client_id, client);
2477
2478 assert!(manager.check_and_remove_expired(client_id));
2479
2480 let frame = close_frame
2481 .get()
2482 .expect("the close reason is recorded even though the queue is full");
2483 assert_eq!(frame.code, CloseCode::Policy);
2484 assert_eq!(
2485 frame.reason.as_str(),
2486 "token-expired: Authentication token expired"
2487 );
2488 assert!(matches!(queue.recv().await, Some(Message::Text(_))));
2492 assert!(queue.recv().await.is_none());
2493 }
2494
2495 #[test]
2496 fn unexpired_token_is_kept() {
2497 let manager = ClientManager::new();
2498 let client_id = insert_client(
2499 &manager,
2500 create_test_auth_context("user-1", Limits::default()),
2501 );
2502
2503 assert!(!manager.check_and_remove_expired(client_id));
2504 assert!(manager.has_client(client_id));
2505 }
2506
2507 #[tokio::test]
2509 async fn test_rate_limiter_integration() {
2510 use crate::websocket::rate_limiter::{RateLimiterConfig, WebSocketRateLimiter};
2511
2512 let rate_limiter = Arc::new(WebSocketRateLimiter::new(RateLimiterConfig::default()));
2513 let manager = ClientManager::new().with_rate_limiter(rate_limiter);
2514 let addr = create_test_socket_addr("127.0.0.1");
2515
2516 let auth = create_test_auth_context("user-1", Limits::default());
2518 assert!(manager
2519 .check_connection_allowed(addr, &Some(auth))
2520 .await
2521 .is_ok());
2522 }
2523}