diff --git a/src/Helpers/InnerNetClientHelper.cs b/src/Helpers/InnerNetClientHelper.cs index 305235a..c41f294 100644 --- a/src/Helpers/InnerNetClientHelper.cs +++ b/src/Helpers/InnerNetClientHelper.cs @@ -2,6 +2,7 @@ using Hazel; using InnerNet; using System.Globalization; +using System.Text; namespace BetterAmongUs.Helpers; @@ -38,6 +39,97 @@ internal static class InnerNetClientHelper } } + /// + /// Determines whether the next string value in the specified reader matches the provided string without advancing + /// the reader's position. + /// + /// The message reader to inspect for the presence of the string. Cannot be null. + /// The string to compare against the next value in the reader. If null or empty, the method returns . + /// true if the next string value in the reader matches the specified string; otherwise, false. + /// Thrown if is null. + internal static bool HasString(this MessageReader reader, string @string) + { + if (reader == null) throw new ArgumentNullException(nameof(reader)); + if (string.IsNullOrEmpty(@string)) return true; + + // Save current position + int originalPosition = reader.Position; + int originalReadHead = reader.readHead; + + try + { + // Check if we have enough bytes for at least the packed int length + if (reader.BytesRemaining < 1) + return false; + + // Try to read the packed int length without modifying internal state + int lengthOffset = reader.Position; + int lengthReadHead = reader.readHead; + + // Read packed int manually without affecting the reader's state + bool readMore = true; + int shift = 0; + uint stringLength = 0; + + while (readMore) + { + if (lengthOffset >= reader.Length) + return false; // Not enough data + + byte b = reader.Buffer[reader.Offset + lengthOffset]; + lengthOffset++; + + if (b >= 0x80) + { + readMore = true; + b ^= 0x80; + } + else + { + readMore = false; + } + + stringLength |= (uint)(b << shift); + shift += 7; + + // Sanity check: string length shouldn't be ridiculously large + if (stringLength > reader.Buffer.Length || stringLength > 1024 * 1024) // 1MB max + return false; + } + + // Check if we have enough bytes for the string content + if (reader.Length - lengthOffset < (int)stringLength) + return false; + + // Compare the string + if (stringLength != @string.Length) + return false; + + byte[] stringBytes = Encoding.UTF8.GetBytes(@string); + + // Compare byte by byte + for (int i = 0; i < stringLength; i++) + { + if (reader.Buffer[reader.Offset + lengthOffset + i] != stringBytes[i]) + return false; + } + + return true; + } + catch + { + // If anything goes wrong, assume the string is not present + return false; + } + finally + { + // Restore original position + reader._position = originalPosition; + reader.readHead = originalReadHead; + } + } + /// /// Writes an array of boolean values to a MessageWriter in a packed format to save space. /// Each byte stores up to 8 boolean values as bits. @@ -46,7 +138,7 @@ internal static class InnerNetClientHelper /// The boolean values to write. internal static void WriteBooleans(this MessageWriter writer, IEnumerable boolsEnumerable) { - bool[] bools = boolsEnumerable.ToArray(); + bool[] bools = [.. boolsEnumerable]; writer.Write(bools.Length); diff --git a/src/Managers/NetworkManager.cs b/src/Managers/NetworkManager.cs index 49013c0..3e8d6f2 100644 --- a/src/Managers/NetworkManager.cs +++ b/src/Managers/NetworkManager.cs @@ -271,17 +271,18 @@ internal static class NetworkManager break; } + if (innerNetObject is PlayerControl player && player != null) + { + if (rpcCall == (byte)RpcCalls.SetNamePlateStr) + { + RPC.HandleCustomRPC(player, reader); + } + } + if (Enum.IsDefined(typeof(RpcCalls), rpcCall)) { innerNetObject?.HandleRpc(rpcCall, reader); } - else - { - if (innerNetObject is PlayerControl player && player != null) - { - RPC.HandleCustomRPC(player, rpcCall, reader); - } - } break; } diff --git a/src/Modules/AntiCheat/BetterAntiCheat.cs b/src/Modules/AntiCheat/BetterAntiCheat.cs index 6ffc221..9b8bac2 100644 --- a/src/Modules/AntiCheat/BetterAntiCheat.cs +++ b/src/Modules/AntiCheat/BetterAntiCheat.cs @@ -5,6 +5,7 @@ using BetterAmongUs.Helpers; using BetterAmongUs.Managers; using BetterAmongUs.Modules.Support; using BetterAmongUs.Mono; +using BetterAmongUs.Network; using BetterAmongUs.Patches.Gameplay.UI.Settings; using HarmonyLib; using Hazel; @@ -188,6 +189,15 @@ internal static class BetterAntiCheat return false; } + if (callId == (byte)RpcCalls.SetNamePlateStr) + { + if (RPC.IsPackedCustomRpc(reader)) + { + reader.Recycle(); + return false; + } + } + if (!player.IsHost()) { if (callId is (byte)RpcCalls.SetTasks @@ -207,11 +217,11 @@ internal static class BetterAntiCheat if (GameState.IsInGamePlay) { if (callId is (byte)RpcCalls.SetColor - or (byte)RpcCalls.SetHat_Deprecated - or (byte)RpcCalls.SetSkin_Deprecated - or (byte)RpcCalls.SetVisor_Deprecated - or (byte)RpcCalls.SetPet_Deprecated - or (byte)RpcCalls.SetNamePlate_Deprecated) + or (byte)RpcCalls.SetHatStr + or (byte)RpcCalls.SetSkinStr + or (byte)RpcCalls.SetVisorStr + or (byte)RpcCalls.SetPetStr + or (byte)RpcCalls.SetNamePlateStr) { if (BetterNotificationManager.NotifyCheat(player, string.Format(Translator.GetString("AntiCheat.InvalidSetRPC"), Enum.GetName((RpcCalls)callId)))) { diff --git a/src/Modules/AntiCheat/RPCHandlers/CheckAppearHandler.cs b/src/Modules/AntiCheat/RPCHandlers/CheckAppearHandler.cs index bea7e19..b60a080 100644 --- a/src/Modules/AntiCheat/RPCHandlers/CheckAppearHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/CheckAppearHandler.cs @@ -40,7 +40,6 @@ internal sealed class CheckAppearHandler : RPCHandler { if (!GameState.IsHost) { - LogRpcInfo($"Non-host attempted CheckAppear RPC."); return false; } diff --git a/src/Modules/AntiCheat/RPCHandlers/CheckColorHandler.cs b/src/Modules/AntiCheat/RPCHandlers/CheckColorHandler.cs index 5331804..339d67f 100644 --- a/src/Modules/AntiCheat/RPCHandlers/CheckColorHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/CheckColorHandler.cs @@ -12,7 +12,6 @@ internal sealed class CheckColorHandler : RPCHandler { if (!GameState.IsHost) { - LogRpcInfo($"Non-host attempted CheckColor RPC"); return false; } diff --git a/src/Modules/AntiCheat/RPCHandlers/CheckMurderHandler.cs b/src/Modules/AntiCheat/RPCHandlers/CheckMurderHandler.cs index cd086fb..7ce5c15 100644 --- a/src/Modules/AntiCheat/RPCHandlers/CheckMurderHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/CheckMurderHandler.cs @@ -60,7 +60,6 @@ internal sealed class CheckMurderHandler : RPCHandler { if (!GameState.IsHost) { - LogRpcInfo($"Non-host attempted CheckMurder RPC"); return false; } diff --git a/src/Modules/AntiCheat/RPCHandlers/CheckNameHandler.cs b/src/Modules/AntiCheat/RPCHandlers/CheckNameHandler.cs index 129d8cb..a4e4417 100644 --- a/src/Modules/AntiCheat/RPCHandlers/CheckNameHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/CheckNameHandler.cs @@ -15,7 +15,6 @@ internal sealed class CheckNameHandler : RPCHandler { if (!GameState.IsHost) { - LogRpcInfo($"Non-host attempted CheckName RPC"); return false; } diff --git a/src/Modules/AntiCheat/RPCHandlers/CheckProtectHandler.cs b/src/Modules/AntiCheat/RPCHandlers/CheckProtectHandler.cs index a69ab05..85facfe 100644 --- a/src/Modules/AntiCheat/RPCHandlers/CheckProtectHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/CheckProtectHandler.cs @@ -49,7 +49,6 @@ internal sealed class CheckProtectHandler : RPCHandler { if (!GameState.IsHost) { - LogRpcInfo($"Non-host attempted CheckProtect RPC"); return false; } diff --git a/src/Modules/AntiCheat/RPCHandlers/CheckShapeshiftHandler.cs b/src/Modules/AntiCheat/RPCHandlers/CheckShapeshiftHandler.cs index 38227c7..7244e3d 100644 --- a/src/Modules/AntiCheat/RPCHandlers/CheckShapeshiftHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/CheckShapeshiftHandler.cs @@ -52,7 +52,6 @@ internal sealed class CheckShapeshiftHandler : RPCHandler { if (!GameState.IsHost) { - LogRpcInfo($"Non-host attempted CheckShapeshift RPC"); return false; } diff --git a/src/Modules/AntiCheat/RPCHandlers/CheckSporeHandler.cs b/src/Modules/AntiCheat/RPCHandlers/CheckSporeHandler.cs index 5556466..585ae73 100644 --- a/src/Modules/AntiCheat/RPCHandlers/CheckSporeHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/CheckSporeHandler.cs @@ -12,7 +12,6 @@ internal sealed class CheckSporeHandler : RPCHandler { if (!GameState.IsHost) { - LogRpcInfo($"Non-host attempted CheckSpore RPC"); return false; } diff --git a/src/Modules/AntiCheat/RPCHandlers/CheckVanishHandler.cs b/src/Modules/AntiCheat/RPCHandlers/CheckVanishHandler.cs index 960b6cf..7878f70 100644 --- a/src/Modules/AntiCheat/RPCHandlers/CheckVanishHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/CheckVanishHandler.cs @@ -40,7 +40,6 @@ internal sealed class CheckVanishHandler : RPCHandler { if (!GameState.IsHost) { - LogRpcInfo($"Non-host attempted CheckVanish RPC"); return false; } diff --git a/src/Modules/AntiCheat/RPCHandlers/CheckZiplineHandler.cs b/src/Modules/AntiCheat/RPCHandlers/CheckZiplineHandler.cs index e73eb04..569b6b7 100644 --- a/src/Modules/AntiCheat/RPCHandlers/CheckZiplineHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/CheckZiplineHandler.cs @@ -12,7 +12,6 @@ internal sealed class CheckZiplineHandler : RPCHandler { if (!GameState.IsHost) { - LogRpcInfo($"Non-host attempted CheckZipline RPC"); return false; } diff --git a/src/Modules/HandshakeHandler.cs b/src/Modules/HandshakeHandler.cs index 70f55a2..361cebd 100644 --- a/src/Modules/HandshakeHandler.cs +++ b/src/Modules/HandshakeHandler.cs @@ -3,6 +3,7 @@ using BetterAmongUs.Data; using BetterAmongUs.Enums; using BetterAmongUs.Helpers; using BetterAmongUs.Mono; +using BetterAmongUs.Network; using Hazel; using Il2CppInterop.Runtime.Attributes; using System.Collections; @@ -71,10 +72,11 @@ internal sealed class HandshakeHandler if (HasSendSharedSecret) return; HasSendSharedSecret = true; - var writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SendSecretToPlayer, SendOption.Reliable, extendedData._Data.ClientId); - writer.WriteBytes(SharedSecret.GetPublicKey()); - writer.Write(SharedSecret.GetTempKey()); - AmongUsClient.Instance.FinishRpcImmediately(writer); + RPC.SendCustomRpc(CustomRPC.SendSecretToPlayer, writer => + { + writer.WriteBytes(SharedSecret.GetPublicKey()); + writer.Write(SharedSecret.GetTempKey()); + }, extendedData._Data.ClientId); } /// @@ -116,10 +118,11 @@ internal sealed class HandshakeHandler int hash = SharedSecret.GetSharedSecretHash(); // Logger.Log($"Sending secret hash: {hash} (tempKey: {tempKey})"); - var writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.CheckSecretHashFromPlayer, SendOption.Reliable, senderClientId); - writer.Write(tempKey); - writer.Write(hash); - AmongUsClient.Instance.FinishRpcImmediately(writer); + RPC.SendCustomRpc(CustomRPC.CheckSecretHashFromPlayer, writer => + { + writer.Write(tempKey); + writer.Write(hash); + }, senderClientId); } /// diff --git a/src/Network/RPC.cs b/src/Network/RPC.cs index 08d891b..8ba1648 100644 --- a/src/Network/RPC.cs +++ b/src/Network/RPC.cs @@ -1,6 +1,6 @@ -using BetterAmongUs.Enums; +using AmongUs.Data; +using BetterAmongUs.Enums; using BetterAmongUs.Helpers; -using BetterAmongUs.Modules; using BetterAmongUs.Mono; using Hazel; @@ -8,86 +8,98 @@ namespace BetterAmongUs.Network; /// /// Handles custom RPC (Remote Procedure Call) messages for BetterAmongUs. +/// This class provides methods for sending and receiving custom RPC messages +/// packed within vanilla RPC calls to maintain compatibility with vanilla servers. /// internal static class RPC { /// - /// Processes custom RPC messages received from other players. + /// The flag string used to identify custom RPC messages packed within vanilla RPC calls. /// - /// The player who sent the RPC. - /// The ID of the RPC call. - /// The message reader containing the RPC data. - /// - /// Handles both defined custom RPCs and protects against unknown RPCs in modded lobbies. - /// - internal static void HandleCustomRPC(PlayerControl player, byte callId, MessageReader oldReader) + internal const string CUSTOM_RPC_FLAG = "bau:rpc"; + + /// + /// Sends a custom RPC message packed within a vanilla SetNamePlateStr RPC call. + /// This method is used to maintain compatibility with vanilla Among Us servers + /// while allowing custom RPC communication. + /// + /// The custom RPC type to send. + /// A delegate that writes the custom RPC payload to the message writer. + /// The specific client ID to target, or -1 to broadcast to all clients. + internal static void SendCustomRpc(CustomRPC customRPC, Action action, int targetClientId = -1) { - if (player == null || player.IsLocalPlayer() || player.Data == null || Enum.IsDefined(typeof(RpcCalls), callId)) return; + var writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)RpcCalls.SetNamePlateStr, SendOption.Reliable, targetClientId); - if (Enum.IsDefined(typeof(CustomRPC), (int)unchecked(callId))) + writer.Write(DataManager.Player.Customization.NamePlate); + writer.Write(PlayerControl.LocalPlayer.GetNextRpcSequenceId(RpcCalls.SetNamePlateStr)); + + writer.Write(CUSTOM_RPC_FLAG); // Flag to check if its a rpc packed into SetNamePlateStr + writer.Write((byte)customRPC); + action(writer); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + + /// + /// Handles incoming custom RPC messages by extracting and processing them + /// from the packed SetNamePlateStr RPC call. + /// + /// The player who sent the RPC message. + /// The message reader containing the RPC data. + internal static void HandleCustomRPC(PlayerControl player, MessageReader oldReader) + { + if (player == null || player.IsLocalPlayer() || player.Data == null) return; + + MessageReader reader = MessageReader.Get(oldReader); + + _ = reader.ReadString(); + _ = reader.ReadByte(); + + if (IsPackedCustomRpc(reader)) { - MessageReader reader = MessageReader.Get(oldReader); - - switch (callId) + CustomRPC customRPC = (CustomRPC)reader.ReadByte(); + switch (customRPC) { - /* - case (byte)CustomRPC.LegacyBetterCheck: - { - var SetBetterUser = reader.ReadBoolean(); - var Signature = reader.ReadString(); - var Version = reader.ReadString(); - var IsVerified = Signature == Main.ModSignature.ToString(); - - if (string.IsNullOrEmpty(Signature) || string.IsNullOrEmpty(Version)) - { - BetterNotificationManager.NotifyCheat(player, $"Invalid Action RPC: {Enum.GetName((CustomRPC)callId)} called with invalid info"); - break; - } - - player.BetterData().IsBetterUser = SetBetterUser; - - if (IsVerified) - { - player.BetterData().IsVerifiedBetterUser = true; - } - - Logger.Log($"Received better user RPC from: {player.Data.PlayerName}:{player.Data.FriendCode}:{Utils.GetHashPuid(player)} - " + - $"BetterUser: {SetBetterUser} - " + - $"Version: {Version} - " + - $"Verified: {IsVerified} - " + - $"Signature: {Signature}"); - - Utils.DirtyAllNames(); - } - break; - */ - case (byte)CustomRPC.SendSecretToPlayer: + case CustomRPC.SendSecretToPlayer: { player.BetterData().HandshakeHandler.HandleSecretFromSender(reader); } break; - case (byte)CustomRPC.CheckSecretHashFromPlayer: + case CustomRPC.CheckSecretHashFromPlayer: { player.BetterData().HandshakeHandler.HandleSecretHashFromPlayer(reader); } break; } } - else if (!Enum.IsDefined(typeof(CustomRPC), (int)unchecked(callId))) + + reader.Recycle(); + } + + /// + /// Determines whether a MessageReader contains a packed custom RPC message. + /// + /// The MessageReader to check for custom RPC content. + /// + /// true if the reader contains a custom RPC flag and custom RPC data; + /// otherwise, false. + /// + internal static bool IsPackedCustomRpc(MessageReader reader) + { + if (reader.BytesRemaining > 0) { try { - if (!GameState.IsHost) + if (reader.ReadString() == CUSTOM_RPC_FLAG) { - if (player.IsHost()) - { - var Icon = Translator.GetString("BAUMark"); - var BAU = $"{Icon}{Translator.GetString("BAU")}{Icon}"; - Utils.DisconnectSelf(string.Format(Translator.GetString("ModdedLobbyMsg"), BAU)); - } + return true; } } - catch { } + catch + { + return false; + } } + + return false; } } \ No newline at end of file