@@ -33,8 +33,8 @@ use crate::api::{
3333
3434/// Max UUIDs in a single `SubscribePlayers` or `GetActiveCosmetics` message.
3535const MAX_PLAYERS_PER_REQUEST : usize = 64 ;
36- /// Max distinct players a connection may subscribe to at once (render distance).
37- const MAX_PLAYER_SUBSCRIPTIONS : usize = 128 ;
36+ const MAX_PLAYER_SUBSCRIPTIONS : usize = 512 ;
37+ const REAL_PLAYER_UUID_VERSION : usize = 4 ;
3838
3939fn enforce_max_players_per_request ( players : & [ Uuid ] ) -> Result < ( ) , WebsocketError > {
4040 if players. len ( ) > MAX_PLAYERS_PER_REQUEST {
@@ -45,6 +45,21 @@ fn enforce_max_players_per_request(players: &[Uuid]) -> Result<(), WebsocketErro
4545 Ok ( ( ) )
4646}
4747
48+ fn is_fake_player ( player : & Uuid ) -> bool {
49+ player. get_version_num ( ) != REAL_PLAYER_UUID_VERSION
50+ }
51+
52+ fn empty_snapshot ( rejected : Vec < Uuid > , request_id : Option < u64 > ) -> ClientBoundPacket {
53+ ClientBoundPacket :: SubscriptionSnapshot {
54+ equipped : HashMap :: new ( ) ,
55+ active_emotes : HashMap :: new ( ) ,
56+ particle_colors : HashMap :: new ( ) ,
57+ users : Vec :: new ( ) ,
58+ rejected,
59+ request_id,
60+ }
61+ }
62+
4863pub ( super ) fn router ( ) -> ApiRouter < ApiState > {
4964 ApiRouter :: new ( )
5065 . route ( "/websocket" , get ( self :: endpoint) )
@@ -377,42 +392,49 @@ async fn subscribe(
377392 state : & ApiState ,
378393 connection_id : ConnectionId ,
379394 players : Vec < Uuid > ,
395+ request_id : Option < u64 > ,
380396) -> Result < ClientBoundPacket , WebsocketError > {
381- let requested = players. into_iter ( ) . collect :: < HashSet < _ > > ( ) ;
382- let newly_subscribed = {
397+ let mut seen = HashSet :: new ( ) ;
398+ let requested = players
399+ . into_iter ( )
400+ . filter ( |player| seen. insert ( * player) )
401+ . collect :: < Vec < _ > > ( ) ;
402+
403+ let ( newly_subscribed, rejected) = {
383404 let mut connections = state. realtime . connections . write ( ) . await ;
384405 let Some ( connection) = connections. get_mut ( & connection_id) else {
385- return Ok ( ClientBoundPacket :: SubscriptionSnapshot {
386- equipped : HashMap :: new ( ) ,
387- active_emotes : HashMap :: new ( ) ,
388- particle_colors : HashMap :: new ( ) ,
389- users : Vec :: new ( ) ,
390- } ) ;
406+ return Ok ( empty_snapshot ( Vec :: new ( ) , request_id) ) ;
391407 } ;
392408
393- let pending = requested
394- . iter ( )
395- . filter ( |player| !connection. subscriptions . contains ( player) )
396- . count ( ) ;
397- if connection. subscriptions . len ( ) + pending > MAX_PLAYER_SUBSCRIPTIONS {
398- return Err ( WebsocketError :: SubscriptionLimitExceeded {
399- limit : MAX_PLAYER_SUBSCRIPTIONS ,
400- } ) ;
409+ let mut newly_subscribed = Vec :: new ( ) ;
410+ let mut rejected = Vec :: new ( ) ;
411+ for player in requested {
412+ if is_fake_player ( & player) {
413+ rejected. push ( player) ;
414+ continue ;
415+ }
416+ if connection. subscriptions . contains ( & player) {
417+ continue ;
418+ }
419+ if connection. subscriptions . len ( ) >= MAX_PLAYER_SUBSCRIPTIONS {
420+ rejected. push ( player) ;
421+ continue ;
422+ }
423+ connection. subscriptions . insert ( player) ;
424+ newly_subscribed. push ( player) ;
401425 }
402-
403- requested
404- . into_iter ( )
405- . filter ( |player| connection. subscriptions . insert ( * player) )
406- . collect :: < Vec < _ > > ( )
426+ ( newly_subscribed, rejected)
407427 } ;
408428
429+ if !rejected. is_empty ( ) {
430+ warn ! (
431+ "Connection {connection_id} rejected {} subscription(s) (cap {MAX_PLAYER_SUBSCRIPTIONS})" ,
432+ rejected. len( )
433+ ) ;
434+ }
435+
409436 if newly_subscribed. is_empty ( ) {
410- return Ok ( ClientBoundPacket :: SubscriptionSnapshot {
411- equipped : HashMap :: new ( ) ,
412- active_emotes : HashMap :: new ( ) ,
413- particle_colors : HashMap :: new ( ) ,
414- users : Vec :: new ( ) ,
415- } ) ;
437+ return Ok ( empty_snapshot ( rejected, request_id) ) ;
416438 }
417439
418440 {
@@ -480,6 +502,8 @@ async fn subscribe(
480502 active_emotes,
481503 particle_colors,
482504 users,
505+ rejected,
506+ request_id,
483507 } )
484508}
485509
@@ -530,14 +554,28 @@ async fn broadcast_to_watchers(
530554 }
531555}
532556
557+ struct RequestError {
558+ error : WebsocketError ,
559+ request_id : Option < u64 > ,
560+ }
561+
562+ impl From < WebsocketError > for RequestError {
563+ fn from ( error : WebsocketError ) -> Self {
564+ Self {
565+ error,
566+ request_id : None ,
567+ }
568+ }
569+ }
570+
533571async fn handle_msg (
534572 socket : & mut WebSocket ,
535573 state : & ApiState ,
536574 player : & entities:: user:: Model ,
537575 connection_id : ConnectionId ,
538576 msg : Result < Message , axum:: Error > ,
539- ) -> Result < ( ) , WebsocketError > {
540- let msg = msg?;
577+ ) -> Result < ( ) , RequestError > {
578+ let msg = msg. map_err ( WebsocketError :: from ) ?;
541579
542580 // Ignore control/keepalive frames. Ping/Pong carry an opaque payload (Ktor
543581 // sends a Ping every pingInterval) that is not a serializable request, and
@@ -548,7 +586,20 @@ async fn handle_msg(
548586
549587 let parsed = serde_json:: from_slice :: < ServerBoundPacket > ( & msg. into_data ( ) )
550588 . map_err ( WebsocketError :: Deserialization ) ?;
589+ let request_id = parsed. request_id ( ) ;
551590
591+ handle_packet ( socket, state, player, connection_id, parsed)
592+ . await
593+ . map_err ( |error| RequestError { error, request_id } )
594+ }
595+
596+ async fn handle_packet (
597+ socket : & mut WebSocket ,
598+ state : & ApiState ,
599+ player : & entities:: user:: Model ,
600+ connection_id : ConnectionId ,
601+ parsed : ServerBoundPacket ,
602+ ) -> Result < ( ) , WebsocketError > {
552603 match parsed {
553604 ServerBoundPacket :: GetActiveCosmetics { players } => {
554605 enforce_max_players_per_request ( & players) ?;
@@ -560,9 +611,12 @@ async fn handle_msg(
560611 )
561612 . await ?;
562613 }
563- ServerBoundPacket :: SubscribePlayers { players } => {
614+ ServerBoundPacket :: SubscribePlayers {
615+ players,
616+ request_id,
617+ } => {
564618 enforce_max_players_per_request ( & players) ?;
565- let snapshot = subscribe ( state, connection_id, players) . await ?;
619+ let snapshot = subscribe ( state, connection_id, players, request_id ) . await ?;
566620 send_packet ( socket, snapshot) . await ?;
567621 }
568622 ServerBoundPacket :: UnsubscribePlayers { players } => {
@@ -670,8 +724,14 @@ async fn endpoint(
670724 let equipped = match load_equipped ( & state, player. id ) . await {
671725 Ok ( equipped) => equipped,
672726 Err ( error) => {
673- let _ =
674- send_packet ( & mut socket, ClientBoundPacket :: Error { error } ) . await ;
727+ let _ = send_packet (
728+ & mut socket,
729+ ClientBoundPacket :: Error {
730+ error,
731+ request_id : None ,
732+ } ,
733+ )
734+ . await ;
675735 return ;
676736 }
677737 } ;
@@ -697,15 +757,18 @@ async fn endpoint(
697757 let Some ( packet) = packet else {
698758 break ;
699759 } ;
700- send_packet( & mut socket, packet) . await
760+ send_packet( & mut socket, packet) . await . map_err ( RequestError :: from )
701761 }
702762 } ;
703763
704764 match result {
705765 Ok ( _) => continue ,
706- Err ( WebsocketError :: Fatal ( _) ) => break ,
707- Err ( e) => {
708- let e = ClientBoundPacket :: Error { error : e } ;
766+ Err ( RequestError {
767+ error : WebsocketError :: Fatal ( _) ,
768+ ..
769+ } ) => break ,
770+ Err ( RequestError { error, request_id } ) => {
771+ let e = ClientBoundPacket :: Error { error, request_id } ;
709772 if send_packet ( & mut socket, e) . await . is_err ( ) {
710773 break ;
711774 } ;
0 commit comments