From 01721feeeb2c795e4fcd665d2235f62d844273cc Mon Sep 17 00:00:00 2001 From: James Frowen Date: Tue, 21 Jan 2025 23:01:47 +0000 Subject: [PATCH] feat: adding allowServerToCall option to ServerRpc Adding option to allow ServerRpc to be called on server and bypass authority check --- Assets/Mirage/Runtime/CustomAttributes.cs | 5 + .../Runtime/RemoteCalls/ServerRpcSender.cs | 10 +- .../Weaver/Processors/ServerRpcProcessor.cs | 14 +- .../RpcTests/RpcUsageClientServerTest.cs | 304 +++++++++++++++++- .../Runtime/RpcTests/RpcUsageHostTest.cs | 84 +++++ 5 files changed, 405 insertions(+), 12 deletions(-) diff --git a/Assets/Mirage/Runtime/CustomAttributes.cs b/Assets/Mirage/Runtime/CustomAttributes.cs index fb0b4e6717..00ee1c21e6 100644 --- a/Assets/Mirage/Runtime/CustomAttributes.cs +++ b/Assets/Mirage/Runtime/CustomAttributes.cs @@ -83,6 +83,11 @@ public class ServerRpcAttribute : Attribute { public Channel channel = Channel.Reliable; public bool requireAuthority = true; + + /// + /// Allows the server to invoke the method locally. Note: this will bypass any authority checks on host. + /// + public bool allowServerToCall = false; } /// diff --git a/Assets/Mirage/Runtime/RemoteCalls/ServerRpcSender.cs b/Assets/Mirage/Runtime/RemoteCalls/ServerRpcSender.cs index 813271b81f..3294b78e92 100644 --- a/Assets/Mirage/Runtime/RemoteCalls/ServerRpcSender.cs +++ b/Assets/Mirage/Runtime/RemoteCalls/ServerRpcSender.cs @@ -77,13 +77,15 @@ private static void Validate(NetworkBehaviour behaviour, int index, bool require /// /// player used for RpcTarget.Player /// - public static bool ShouldInvokeLocally(NetworkBehaviour behaviour, bool requireAuthority) + public static bool ShouldInvokeLocally(NetworkBehaviour behaviour, bool requireAuthority, bool allowServerToCall) { + // if allowServerToCall, then just check server because we ignore all other checks + if (behaviour.IsServer && allowServerToCall) + return true; + // not client? error if (!behaviour.IsClient) - { throw new InvalidOperationException("Server RPC can only be called when client is active"); - } // not host? never invoke locally if (!behaviour.IsServer) @@ -91,9 +93,7 @@ public static bool ShouldInvokeLocally(NetworkBehaviour behaviour, bool requireA // check if auth is required and that host has auth over the object if (requireAuthority && !behaviour.HasAuthority) - { throw new InvalidOperationException($"Trying to send ServerRpc for object without authority."); - } return true; } diff --git a/Assets/Mirage/Weaver/Processors/ServerRpcProcessor.cs b/Assets/Mirage/Weaver/Processors/ServerRpcProcessor.cs index f27abea1b2..9402f3de47 100644 --- a/Assets/Mirage/Weaver/Processors/ServerRpcProcessor.cs +++ b/Assets/Mirage/Weaver/Processors/ServerRpcProcessor.cs @@ -51,6 +51,7 @@ private MethodDefinition GenerateStub(MethodDefinition md, CustomAttribute serve { var channel = serverRpcAttr.GetField(nameof(ServerRpcAttribute.channel), 0); var requireAuthority = serverRpcAttr.GetField(nameof(ServerRpcAttribute.requireAuthority), true); + var allowServerToCall = serverRpcAttr.GetField(nameof(ServerRpcAttribute.allowServerToCall), false); var cmd = SubstituteMethod(md); @@ -61,7 +62,7 @@ private MethodDefinition GenerateStub(MethodDefinition md, CustomAttribute serve // call the body // return; // } - CallBody(worker, cmd, requireAuthority); + CallBody(worker, cmd, requireAuthority, allowServerToCall); // NetworkWriter writer = NetworkWriterPool.GetWriter() var writer = md.AddLocal(); @@ -91,13 +92,14 @@ private MethodDefinition GenerateStub(MethodDefinition md, CustomAttribute serve return cmd; } - public void InvokeLocally(ILProcessor worker, bool requiredAuthority, Action body) + public void InvokeLocally(ILProcessor worker, bool requireAuthority, bool allowServerToCall, Action body) { // if (IsServer) { var endif = worker.Create(OpCodes.Nop); worker.Append(worker.Create(OpCodes.Ldarg_0)); - worker.Append(worker.Create(requiredAuthority.OpCode_Ldc())); - worker.Append(worker.Create(OpCodes.Call, () => ServerRpcSender.ShouldInvokeLocally(default, default))); + worker.Append(worker.Create(requireAuthority.OpCode_Ldc())); + worker.Append(worker.Create(allowServerToCall.OpCode_Ldc())); + worker.Append(worker.Create(OpCodes.Call, () => ServerRpcSender.ShouldInvokeLocally(default, default, default))); worker.Append(worker.Create(OpCodes.Brfalse, endif)); body(); @@ -107,9 +109,9 @@ public void InvokeLocally(ILProcessor worker, bool requiredAuthority, Action bod } - private void CallBody(ILProcessor worker, MethodDefinition rpc, bool requiredAuthority) + private void CallBody(ILProcessor worker, MethodDefinition rpc, bool requireAuthority, bool allowServerToCall) { - InvokeLocally(worker, requiredAuthority, () => + InvokeLocally(worker, requireAuthority, allowServerToCall, () => { InvokeBody(worker, rpc); worker.Append(worker.Create(OpCodes.Ret)); diff --git a/Assets/Tests/Runtime/RpcTests/RpcUsageClientServerTest.cs b/Assets/Tests/Runtime/RpcTests/RpcUsageClientServerTest.cs index f1e62234d9..8c03cf90a2 100644 --- a/Assets/Tests/Runtime/RpcTests/RpcUsageClientServerTest.cs +++ b/Assets/Tests/Runtime/RpcTests/RpcUsageClientServerTest.cs @@ -42,6 +42,7 @@ public void RpcObservers(short arg1) Called?.Invoke(arg1); } } + public class RpcUsageBehaviour_RequireAuthority : NetworkBehaviour, IRpcUsageBehaviour { public event Action Called; @@ -62,6 +63,27 @@ public void RpcIgnoreAuthority(short arg1) Called?.Invoke(arg1); } } + public class RpcUsageBehaviour_AllowServerRequireAuthority : NetworkBehaviour, IRpcUsageBehaviour + { + public event Action Called; + + [ServerRpc(requireAuthority = true, allowServerToCall = true)] + public void RpcAllowServerRequireAuthority(short arg1) + { + Called?.Invoke(arg1); + } + } + + public class RpcUsageBehaviour_AllowServerIgnoreAuthority : NetworkBehaviour, IRpcUsageBehaviour + { + public event Action Called; + + [ServerRpc(requireAuthority = false, allowServerToCall = true)] + public void RpcAllowServerIgnoreAuthority(short arg1) + { + Called?.Invoke(arg1); + } + } public abstract class RpcUsageClientServerTestBase : MultiRemoteClientSetup where T : NetworkBehaviour, IRpcUsageBehaviour { @@ -71,6 +93,11 @@ public abstract class RpcUsageClientServerTestBase : MultiRemoteClientSetup _serverStub; protected Action[] _clientStub; + protected T _noOwnerBehaviourServer; + protected T _noOwnerBehaviourClient; + protected Action _noOwnerStub_OnServer; + protected Action _noOwnerStub_OnClient; + protected override async UniTask LateSetup() { await base.LateSetup(); @@ -87,6 +114,29 @@ protected override async UniTask LateSetup() _clientStub[i] = Substitute.For>(); _remoteClients[i].Get(serverComp).Called += _clientStub[i]; } + + await CreateNoOwnerCharacter(); + } + + private async UniTask CreateNoOwnerCharacter() + { + var identity = UnityEngine.Object.Instantiate(_characterPrefab); + _noOwnerBehaviourServer = identity.GetComponent(); + identity.name = "player (server)"; + // prefab is not active, we set it active here + // note: this is needed for SOM to spawn the object on client + identity.gameObject.SetActive(true); + serverObjectManager.Spawn(identity); + + _noOwnerStub_OnServer = Substitute.For>(); + _noOwnerBehaviourServer.Called += _noOwnerStub_OnServer; + + await UniTask.Yield(); + await UniTask.Yield(); + // get the client copy + _noOwnerBehaviourClient = _remoteClients[0].Get(_noOwnerBehaviourServer); + _noOwnerStub_OnClient = Substitute.For>(); + _noOwnerBehaviourClient.Called += _noOwnerStub_OnClient; } } @@ -167,7 +217,6 @@ public IEnumerator CalledOnAllObservers_AllObservering() _serverStub.DidNotReceiveWithAnyArgs().Invoke(default); } - [UnityTest] public IEnumerator CalledOnAllObservers_SomeObservering() { @@ -311,6 +360,40 @@ public IEnumerator ThrowsWhenCalledOnServer() _serverStub.DidNotReceiveWithAnyArgs().Invoke(default); } + [UnityTest] + public IEnumerator NoOwnerCalledOnServerThrows() + { + var exception = Assert.Throws(() => + { + _noOwnerBehaviourServer.RpcRequireAuthority(NUM); + }); + + Assert.That(exception, Has.Message.EqualTo("Server RPC can only be called when client is active")); + + yield return null; + yield return null; + + _noOwnerStub_OnClient.DidNotReceiveWithAnyArgs().Invoke(default); + _noOwnerStub_OnServer.DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator NoOwnerCalledOnClientThrows() + { + var exception = Assert.Throws(() => + { + _noOwnerBehaviourClient.RpcRequireAuthority(NUM); + }); + + Assert.That(exception, Has.Message.EqualTo("Trying to send ServerRpc for object without authority. Mirage.Tests.Runtime.RpcTests.RpcUsageBehaviour_RequireAuthority.RpcRequireAuthority")); + + yield return null; + yield return null; + + _noOwnerStub_OnClient.DidNotReceiveWithAnyArgs().Invoke(default); + _noOwnerStub_OnServer.DidNotReceiveWithAnyArgs().Invoke(default); + } + [UnityTest] public IEnumerator ThrowsWhenCalledUnSpawnedObject() { @@ -383,6 +466,35 @@ public IEnumerator ThrowsWhenCalledOnServer() _serverStub.DidNotReceiveWithAnyArgs().Invoke(default); } + [UnityTest] + public IEnumerator NoOwnerCalledOnServerThrows() + { + var exception = Assert.Throws(() => + { + _noOwnerBehaviourServer.RpcIgnoreAuthority(NUM); + }); + + Assert.That(exception, Has.Message.EqualTo("Server RPC can only be called when client is active")); + + yield return null; + yield return null; + + _noOwnerStub_OnClient.DidNotReceiveWithAnyArgs().Invoke(default); + _noOwnerStub_OnServer.DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator NoOwnerCalledOnClientCalls() + { + _noOwnerBehaviourClient.RpcIgnoreAuthority(NUM); + + yield return null; + yield return null; + + _noOwnerStub_OnClient.DidNotReceiveWithAnyArgs().Invoke(default); + _noOwnerStub_OnServer.Received(1).Invoke(NUM); + } + [UnityTest] public IEnumerator ThrowsWhenCalledUnSpawnedObject() { @@ -405,4 +517,194 @@ public IEnumerator ThrowsWhenCalledUnSpawnedObject() _serverStub.DidNotReceiveWithAnyArgs().Invoke(default); } } + + public class RpcUsageClientServerTest_AllowServerRequireAuthority : RpcUsageClientServerTestBase + { + [UnityTest] + public IEnumerator CalledWhenCalledWithAuthority() + { + ClientComponent(0).RpcAllowServerRequireAuthority(NUM); + + yield return null; + yield return null; + + _clientStub[0].DidNotReceiveWithAnyArgs().Invoke(default); + _clientStub[1].DidNotReceiveWithAnyArgs().Invoke(default); + _serverStub.Received(1).Invoke(NUM); + } + + [UnityTest] + public IEnumerator ThrowsWhenCalledWithoutAuthority() + { + var exception = Assert.Throws(() => + { + // call character[0] on client[1] + _remoteClients[1].Get(ClientComponent(0)).RpcAllowServerRequireAuthority(NUM); + }); + + // should be full message (see in client) because server is not active + Assert.That(exception, Has.Message.EqualTo("Trying to send ServerRpc for object without authority. Mirage.Tests.Runtime.RpcTests.RpcUsageBehaviour_AllowServerRequireAuthority.RpcAllowServerRequireAuthority")); + + // ensure that none were called, even if exception was throw + yield return null; + yield return null; + + _clientStub[0].DidNotReceiveWithAnyArgs().Invoke(default); + _clientStub[1].DidNotReceiveWithAnyArgs().Invoke(default); + _serverStub.DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator CalledWhenCalledOnServer() + { + ServerComponent(0).RpcAllowServerRequireAuthority(NUM); + _serverStub.Received(1).Invoke(NUM); // should have invoked right away + + yield return null; + yield return null; + + _clientStub[0].DidNotReceiveWithAnyArgs().Invoke(default); + _clientStub[1].DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator NoOwnerCalledOnServer() + { + _noOwnerBehaviourServer.RpcAllowServerRequireAuthority(NUM); + _noOwnerStub_OnServer.Received(1).Invoke(NUM); // should have invoked right away + + yield return null; + yield return null; + + _noOwnerStub_OnClient.DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator NoOwnerCalledOnClientThrows() + { + var exception = Assert.Throws(() => + { + _noOwnerBehaviourClient.RpcAllowServerRequireAuthority(NUM); + }); + + Assert.That(exception, Has.Message.EqualTo("Trying to send ServerRpc for object without authority. Mirage.Tests.Runtime.RpcTests.RpcUsageBehaviour_AllowServerRequireAuthority.RpcAllowServerRequireAuthority")); + + yield return null; + yield return null; + + _noOwnerStub_OnClient.DidNotReceiveWithAnyArgs().Invoke(default); + _noOwnerStub_OnServer.DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator ThrowsWhenCalledUnSpawnedObject() + { + var unspawned = CreateBehaviour(); + unspawned.Called += _serverStub; + + yield return null; + + var exception = Assert.Throws(() => + { + unspawned.RpcAllowServerRequireAuthority(NUM); + }); + + Assert.That(exception, Has.Message.EqualTo("Server RPC can only be called when client is active")); + + // ensure that none were called, even if exception was throw + yield return null; + yield return null; + + _serverStub.DidNotReceiveWithAnyArgs().Invoke(default); + } + } + + public class RpcUsageClientServerTest_AllowServerIgnoreAuthority : RpcUsageClientServerTestBase + { + [UnityTest] + public IEnumerator CalledWhenCalledWithAuthority() + { + ClientComponent(0).RpcAllowServerIgnoreAuthority(NUM); + + yield return null; + yield return null; + + _clientStub[0].DidNotReceiveWithAnyArgs().Invoke(default); + _clientStub[1].DidNotReceiveWithAnyArgs().Invoke(default); + _serverStub.Received(1).Invoke(NUM); + } + + [UnityTest] + public IEnumerator CalledWhenCalledWithOutAuthority() + { + // call character 1 on client 2 + _remoteClients[1].Get(ClientComponent(0)).RpcAllowServerIgnoreAuthority(NUM); + + // ensure that none were called, even if exception was throw + yield return null; + yield return null; + + _clientStub[0].DidNotReceiveWithAnyArgs().Invoke(default); + _clientStub[1].DidNotReceiveWithAnyArgs().Invoke(default); + _serverStub.Received(1).Invoke(NUM); + } + + [UnityTest] + public IEnumerator CalledWhenCalledOnServer() + { + ServerComponent(0).RpcAllowServerIgnoreAuthority(NUM); + _serverStub.Received(1).Invoke(NUM); // should have invoked right away + + yield return null; + yield return null; + _clientStub[0].DidNotReceiveWithAnyArgs().Invoke(default); + _clientStub[1].DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator NoOwnerCalledOnServerThrows() + { + _noOwnerBehaviourServer.RpcAllowServerIgnoreAuthority(NUM); + _noOwnerStub_OnServer.Received(1).Invoke(NUM); // should have invoked right away + + yield return null; + yield return null; + + _noOwnerStub_OnClient.DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator NoOwnerCalledOnClientCalls() + { + _noOwnerBehaviourClient.RpcAllowServerIgnoreAuthority(NUM); + + yield return null; + yield return null; + + _noOwnerStub_OnClient.DidNotReceiveWithAnyArgs().Invoke(default); + _noOwnerStub_OnServer.Received(1).Invoke(NUM); + } + + [UnityTest] + public IEnumerator ThrowsWhenCalledUnSpawnedObject() + { + var unspawned = CreateBehaviour(); + unspawned.Called += _serverStub; + + yield return null; + + var exception = Assert.Throws(() => + { + unspawned.RpcAllowServerIgnoreAuthority(NUM); + }); + + Assert.That(exception, Has.Message.EqualTo("Server RPC can only be called when client is active")); + + // ensure that none were called, even if exception was throw + yield return null; + yield return null; + + _serverStub.DidNotReceiveWithAnyArgs().Invoke(default); + } + } } diff --git a/Assets/Tests/Runtime/RpcTests/RpcUsageHostTest.cs b/Assets/Tests/Runtime/RpcTests/RpcUsageHostTest.cs index e9b0a88d0b..d05cbe5276 100644 --- a/Assets/Tests/Runtime/RpcTests/RpcUsageHostTest.cs +++ b/Assets/Tests/Runtime/RpcTests/RpcUsageHostTest.cs @@ -331,4 +331,88 @@ public IEnumerator ThrowsWhenCalledWithoutAuthority() _client2Stub.DidNotReceiveWithAnyArgs().Invoke(default); } } + + + public class RpcUsageHostTest_AllowServerRequireAuthority : RpcUsageHostTestBase + { + [UnityTest] + public IEnumerator CalledWhenCalledWithAuthority() + { + hostComponent_onHost.Called += _hostStub; + hostComponent_on2.Called += _client2Stub; + + yield return null; + yield return null; + + hostComponent_onHost.RpcAllowServerRequireAuthority(NUM); + + yield return null; + yield return null; + + _hostStub.Received(1).Invoke(NUM); + _client2Stub.DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator CalledWhenCalledWithoutAuthority() + { + client2Component_onHost.Called += _hostStub; + client2Component_on2.Called += _client2Stub; + + yield return null; + yield return null; + + // note: call is allowed, because server bypasses authority check + // call character 2 on client host + client2Component_onHost.RpcAllowServerRequireAuthority(NUM); + + // ensure that none were called, even if exception was throw + yield return null; + yield return null; + + _hostStub.Received(1).Invoke(NUM); + _client2Stub.DidNotReceiveWithAnyArgs().Invoke(default); + } + } + + public class RpcUsageHostTest_AllowServerIgnoreAuthority : RpcUsageHostTestBase + { + [UnityTest] + public IEnumerator CalledWhenCalledWithAuthority() + { + hostComponent_onHost.Called += _hostStub; + hostComponent_on2.Called += _client2Stub; + + yield return null; + yield return null; + + hostComponent_onHost.RpcAllowServerIgnoreAuthority(NUM); + + yield return null; + yield return null; + + _hostStub.Received(1).Invoke(NUM); + _client2Stub.DidNotReceiveWithAnyArgs().Invoke(default); + } + + [UnityTest] + public IEnumerator ThrowsWhenCalledWithoutAuthority() + { + client2Component_onHost.Called += _hostStub; + client2Component_on2.Called += _client2Stub; + + yield return null; + yield return null; + + // call character 2 on client host + client2Component_onHost.RpcAllowServerIgnoreAuthority(NUM); + + // ensure that none were called, even if exception was throw + yield return null; + yield return null; + + _hostStub.Received(1).Invoke(NUM); + _client2Stub.DidNotReceiveWithAnyArgs().Invoke(default); + } + } }