From 1b0dcc9808d8333157cde0ba3866ff673e13bef9 Mon Sep 17 00:00:00 2001 From: Westin Miller Date: Sat, 11 Nov 2017 01:35:00 -0800 Subject: [PATCH] Validate auth ticket event exposed to other parts of Torch and plugins --- Torch.API/Event/IEventManager.cs | 3 +- .../Managers/MultiplayerManagerDedicated.cs | 175 ++++++++++++------ .../MultiplayerManagerDedicatedEventShim.cs | 95 ++++++++++ Torch.Server/Torch.Server.csproj | 1 + Torch/Collections/MTObservableCollection.cs | 8 +- Torch/Event/EventManager.cs | 13 +- Torch/Event/EventShimAttribute.cs | 2 +- 7 files changed, 232 insertions(+), 65 deletions(-) create mode 100644 Torch.Server/Managers/MultiplayerManagerDedicatedEventShim.cs diff --git a/Torch.API/Event/IEventManager.cs b/Torch.API/Event/IEventManager.cs index 038db33..29ad642 100644 --- a/Torch.API/Event/IEventManager.cs +++ b/Torch.API/Event/IEventManager.cs @@ -1,11 +1,12 @@ using System.Runtime.CompilerServices; +using Torch.API.Managers; namespace Torch.API.Event { /// /// Manager class responsible for registration of event handlers. /// - public interface IEventManager + public interface IEventManager : IManager { /// /// Registers all event handler methods contained in the given instance diff --git a/Torch.Server/Managers/MultiplayerManagerDedicated.cs b/Torch.Server/Managers/MultiplayerManagerDedicated.cs index d8427fc..e08521e 100644 --- a/Torch.Server/Managers/MultiplayerManagerDedicated.cs +++ b/Torch.Server/Managers/MultiplayerManagerDedicated.cs @@ -25,8 +25,7 @@ namespace Torch.Server.Managers private static readonly Logger _log = LogManager.GetCurrentClassLogger(); #pragma warning disable 649 - [ReflectedGetter(Name = "m_members")] - private static Func> _members; + [ReflectedGetter(Name = "m_members")] private static Func> _members; [ReflectedGetter(Name = "m_waitingForGroup")] private static Func> _waitingForGroup; #pragma warning restore 649 @@ -37,7 +36,9 @@ namespace Torch.Server.Managers private Dictionary _gameOwnerIds = new Dictionary(); /// - public MultiplayerManagerDedicated(ITorchBase torch) : base(torch) { } + public MultiplayerManagerDedicated(ITorchBase torch) : base(torch) + { + } /// public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId)); @@ -54,7 +55,8 @@ namespace Torch.Server.Managers } /// - public bool IsBanned(ulong steamId) => _isClientBanned.Invoke(MyMultiplayer.Static, steamId) || MySandboxGame.ConfigDedicated.Banned.Contains(steamId); + public bool IsBanned(ulong steamId) => _isClientBanned.Invoke(MyMultiplayer.Static, steamId) || + MySandboxGame.ConfigDedicated.Banned.Contains(steamId); /// public override void Attach() @@ -62,8 +64,10 @@ namespace Torch.Server.Managers base.Attach(); _gameServerValidateAuthTicketReplacer = _gameServerValidateAuthTicketFactory.Invoke(); _gameServerUserGroupStatusReplacer = _gameServerUserGroupStatusFactory.Invoke(); - _gameServerValidateAuthTicketReplacer.Replace(new Action(ValidateAuthTicketResponse), MyGameService.GameServer); - _gameServerUserGroupStatusReplacer.Replace(new Action(UserGroupStatusResponse), MyGameService.GameServer); + _gameServerValidateAuthTicketReplacer.Replace( + new Action(ValidateAuthTicketResponse), MyGameService.GameServer); + _gameServerUserGroupStatusReplacer.Replace(new Action(UserGroupStatusResponse), + MyGameService.GameServer); _log.Info("Inserted steam authentication intercept"); } @@ -80,15 +84,20 @@ namespace Torch.Server.Managers #pragma warning disable 649 - [ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.ValidateAuthTicketResponse), typeof(MyDedicatedServerBase), "GameServer_ValidateAuthTicketResponse")] + [ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.ValidateAuthTicketResponse), + typeof(MyDedicatedServerBase), "GameServer_ValidateAuthTicketResponse")] private static Func _gameServerValidateAuthTicketFactory; - [ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.UserGroupStatusResponse), typeof(MyDedicatedServerBase), "GameServer_UserGroupStatus")] + + [ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.UserGroupStatusResponse), + typeof(MyDedicatedServerBase), "GameServer_UserGroupStatus")] private static Func _gameServerUserGroupStatusFactory; + private ReflectedEventReplacer _gameServerValidateAuthTicketReplacer; private ReflectedEventReplacer _gameServerUserGroupStatusReplacer; #pragma warning restore 649 #region CustomAuth + #pragma warning disable 649 [ReflectedStaticMethod(Type = typeof(MyDedicatedServerBase), Name = "ConvertSteamIDFrom64")] private static Func _convertSteamIDFrom64; @@ -96,78 +105,130 @@ namespace Torch.Server.Managers [ReflectedStaticMethod(Type = typeof(MyGameService), Name = "GetServerAccountType")] private static Func _getServerAccountType; - [ReflectedMethod(Name = "UserAccepted")] - private static Action _userAcceptedImpl; + [ReflectedMethod(Name = "UserAccepted")] private static Action _userAcceptedImpl; [ReflectedMethod(Name = "UserRejected")] private static Action _userRejected; - [ReflectedMethod(Name = "IsClientBanned")] - private static Func _isClientBanned; - [ReflectedMethod(Name = "IsClientKicked")] - private static Func _isClientKicked; + + [ReflectedMethod(Name = "IsClientBanned")] private static Func _isClientBanned; + [ReflectedMethod(Name = "IsClientKicked")] private static Func _isClientKicked; + [ReflectedMethod(Name = "RaiseClientKicked")] private static Action _raiseClientKicked; #pragma warning restore 649 + private const int _waitListSize = 32; + private readonly List _waitingForGroupLocal = new List(_waitListSize); + + private struct WaitingForGroup + { + public readonly ulong SteamId; + public readonly JoinResult Response; + public readonly ulong SteamOwner; + + public WaitingForGroup(ulong id, JoinResult response, ulong owner) + { + SteamId = id; + Response = response; + SteamOwner = owner; + } + } + //Largely copied from SE private void ValidateAuthTicketResponse(ulong steamID, JoinResult response, ulong steamOwner) { _log.Debug($"ValidateAuthTicketResponse(user={steamID}, response={response}, owner={steamOwner}"); - if (IsBanned(steamOwner)) + if (MySandboxGame.ConfigDedicated.GroupID == 0uL) + RunEvent(new ValidateAuthTicketEvent(steamID, steamOwner, response, 0, true, false)); + else if (_getServerAccountType(MySandboxGame.ConfigDedicated.GroupID) != MyGameServiceAccountType.Clan) + UserRejected(steamID, JoinResult.GroupIdInvalid); + else if (MyGameService.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID)) + lock (_waitingForGroupLocal) + { + if (_waitingForGroupLocal.Count >= _waitListSize) + _waitingForGroupLocal.RemoveAt(0); + _waitingForGroupLocal.Add(new WaitingForGroup(steamID, response, steamOwner)); + } + else + UserRejected(steamID, JoinResult.SteamServersOffline); + } + + private void RunEvent(ValidateAuthTicketEvent info) + { + MultiplayerManagerDedicatedEventShim.RaiseValidateAuthTicket(ref info); + + if (info.FutureVerdict == null) { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.BannedByAdmins); - _raiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID); - } - else if (_isClientKicked.Invoke(MyMultiplayer.Static, steamOwner)) - { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.KickedRecently); - _raiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID); - } - if (response != JoinResult.OK) - { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, response); + if (IsBanned(info.SteamOwner) || IsBanned(info.SteamID)) + CommitVerdict(info.SteamID, JoinResult.BannedByAdmins); + else if (_isClientKicked(MyMultiplayer.Static, info.SteamID) || + _isClientKicked(MyMultiplayer.Static, info.SteamOwner)) + CommitVerdict(info.SteamID, JoinResult.KickedRecently); + else if (info.SteamResponse != JoinResult.OK) + CommitVerdict(info.SteamID, info.SteamResponse); + else if (MyMultiplayer.Static.MemberLimit > 0 && + MyMultiplayer.Static.MemberCount + 1 > MyMultiplayer.Static.MemberLimit) + CommitVerdict(info.SteamID, JoinResult.ServerFull); + else if (MySandboxGame.ConfigDedicated.GroupID == 0uL || + MySandboxGame.ConfigDedicated.Administrators.Contains(info.SteamID.ToString()) || + MySandboxGame.ConfigDedicated.Administrators.Contains(_convertSteamIDFrom64(info.SteamID))) + CommitVerdict(info.SteamID, JoinResult.OK); + else if (MySandboxGame.ConfigDedicated.GroupID == info.Group && (info.Member || info.Officer)) + CommitVerdict(info.SteamID, JoinResult.OK); + else + CommitVerdict(info.SteamID, JoinResult.NotInGroup); return; } - if (MyMultiplayer.Static.MemberLimit > 0 && _members.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Count - 1 >= MyMultiplayer.Static.MemberLimit) + + info.FutureVerdict.ContinueWith((task) => { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.ServerFull); - return; - } - if (MySandboxGame.ConfigDedicated.GroupID == 0uL || - MySandboxGame.ConfigDedicated.Administrators.Contains(steamID.ToString()) || - MySandboxGame.ConfigDedicated.Administrators.Contains(_convertSteamIDFrom64(steamID))) - { - this.UserAccepted(steamID); - return; - } - if (_getServerAccountType(MySandboxGame.ConfigDedicated.GroupID) != MyGameServiceAccountType.Clan) - { - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.GroupIdInvalid); - return; - } - if (MyGameService.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID)) - { - _waitingForGroup.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Add(steamID); - return; - } - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.SteamServersOffline); + JoinResult verdict; + if (task.IsFaulted) + { + _log.Error(task.Exception, $"Future validation verdict faulted"); + verdict = JoinResult.TicketCanceled; + } + else + verdict = task.Result; + Torch.Invoke(() => { CommitVerdict(info.SteamID, verdict); }); + }); + } + + private void CommitVerdict(ulong steamId, JoinResult verdict) + { + if (verdict == JoinResult.OK) + UserAccepted(steamId); + else + UserRejected(steamId, verdict); } private void UserGroupStatusResponse(ulong userId, ulong groupId, bool member, bool officer) { - if (groupId == MySandboxGame.ConfigDedicated.GroupID && _waitingForGroup.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Remove(userId)) - { - if (member || officer) - UserAccepted(userId); - else - _userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, userId, JoinResult.NotInGroup); - } + lock (_waitingForGroupLocal) + for (var j = 0; j < _waitingForGroupLocal.Count; j++) + { + var wait = _waitingForGroupLocal[j]; + if (wait.SteamId == userId) + { + RunEvent(new ValidateAuthTicketEvent(wait.SteamId, wait.SteamOwner, wait.Response, groupId, + member, officer)); + _waitingForGroupLocal.RemoveAt(j); + break; + } + } } + + private void UserRejected(ulong steamId, JoinResult reason) + { + _userRejected.Invoke((MyDedicatedServerBase) MyMultiplayer.Static, steamId, reason); + } + private void UserAccepted(ulong steamId) { - _userAcceptedImpl.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamId); + _userAcceptedImpl.Invoke((MyDedicatedServerBase) MyMultiplayer.Static, steamId); base.RaiseClientJoined(steamId); } + #endregion } -} +} \ No newline at end of file diff --git a/Torch.Server/Managers/MultiplayerManagerDedicatedEventShim.cs b/Torch.Server/Managers/MultiplayerManagerDedicatedEventShim.cs new file mode 100644 index 0000000..d0d7433 --- /dev/null +++ b/Torch.Server/Managers/MultiplayerManagerDedicatedEventShim.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NLog; +using Sandbox; +using Torch.API.Event; +using Torch.Event; +using VRage.Network; + +namespace Torch.Server.Managers +{ + [EventShim] + internal static class MultiplayerManagerDedicatedEventShim + { + private static readonly EventList _eventValidateAuthTicket = + new EventList(); + + + internal static void RaiseValidateAuthTicket(ref ValidateAuthTicketEvent info) + { + _eventValidateAuthTicket?.RaiseEvent(ref info); + } + } + +// class AdminsCanJoinExample : IEventHandler +// { +// [EventHandler(SkipCancelled = false)] +// public void Handle(ref ValidateAuthTicketEvent info) +// { +// if (MySandboxGame.ConfigDedicated.Administrators.Contains(info.SteamID.ToString()) +// || (info.ServerGroup == MySandboxGame.ConfigDedicated.GroupID && info.Officer)) +// info.Verdict = JoinResult.OK; +// } +// } + + /// + /// Event that occurs when a player tries to connect to the server. + /// Use these values to choose a , + /// or leave it unset to allow the default logic to handle the request. + /// + public struct ValidateAuthTicketEvent : IEvent + { + /// + /// SteamID of the player + /// + public readonly ulong SteamID; + + /// + /// SteamID of the game owner + /// + public readonly ulong SteamOwner; + + /// + /// The response from steam + /// + public readonly JoinResult SteamResponse; + + /// + /// ID of the queried group, or 0 if no group. + /// + public readonly ulong Group; + + /// + /// Is this person a member of . If no group this is true. + /// + public readonly bool Member; + + /// + /// Is this person an officer of . If no group this is false. + /// + public readonly bool Officer; + + /// + /// A future verdict on this authorization request. If null, let the default logic choose. + /// + public Task FutureVerdict; + + internal ValidateAuthTicketEvent(ulong steamId, ulong steamOwner, JoinResult steamResponse, + ulong serverGroup, bool member, bool officer) + { + SteamID = steamId; + SteamOwner = steamOwner; + SteamResponse = steamResponse; + Group = serverGroup; + Member = member; + Officer = officer; + FutureVerdict = null; + } + + /// + public bool Cancelled => FutureVerdict != null; + } +} \ No newline at end of file diff --git a/Torch.Server/Torch.Server.csproj b/Torch.Server/Torch.Server.csproj index ea5c705..2a75e82 100644 --- a/Torch.Server/Torch.Server.csproj +++ b/Torch.Server/Torch.Server.csproj @@ -198,6 +198,7 @@ + diff --git a/Torch/Collections/MTObservableCollection.cs b/Torch/Collections/MTObservableCollection.cs index 3096bea..f6fa204 100644 --- a/Torch/Collections/MTObservableCollection.cs +++ b/Torch/Collections/MTObservableCollection.cs @@ -38,7 +38,9 @@ namespace Torch.Collections ~MtObservableCollection() { - _flushEventQueue.Dispose(); + Timer queue = _flushEventQueue; + _flushEventQueue = null; + queue?.Dispose(); } /// @@ -208,10 +210,10 @@ namespace Torch.Collections return; _collectionEventQueue.Enqueue(e); // In half a second, flush the events - _flushEventQueue.Change(500, -1); + _flushEventQueue?.Change(500, -1); } - private readonly Timer _flushEventQueue; + private Timer _flushEventQueue; private readonly Queue _collectionEventQueue = new Queue(); diff --git a/Torch/Event/EventManager.cs b/Torch/Event/EventManager.cs index e752322..f3875aa 100644 --- a/Torch/Event/EventManager.cs +++ b/Torch/Event/EventManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -36,7 +37,7 @@ namespace Torch.Event _log.Warn($"Registering type {type.FullName} as an event dispatch type, even though it isn't declared singleton"); var listsFound = 0; RuntimeHelpers.RunClassConstructor(type.TypeHandle); - foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(EventList<>)) { Type eventType = field.FieldType.GenericTypeArguments[0]; @@ -70,7 +71,7 @@ namespace Torch.Event ParameterInfo[] ps = x.GetParameters(); if (ps.Length != 1) return false; - return ps[0].ParameterType.IsByRef && typeof(IEvent).IsAssignableFrom(ps[0].ParameterType); + return ps[0].ParameterType.IsByRef && typeof(IEvent).IsAssignableFrom(ps[0].ParameterType.GetElementType()); }); return exploreType.BaseType != null ? enumerable.Concat(EventHandlers(exploreType.BaseType)) : enumerable; } @@ -78,9 +79,12 @@ namespace Torch.Event /// private static void RegisterHandlerInternal(IEventHandler instance) { + var foundHandler = false; foreach (MethodInfo handler in EventHandlers(instance.GetType())) { - Type eventType = handler.GetParameters()[0].ParameterType; + Type eventType = handler.GetParameters()[0].ParameterType.GetElementType(); + Debug.Assert(eventType != null); + foundHandler = true; if (eventType.IsInterface) { var foundList = false; @@ -100,6 +104,9 @@ namespace Torch.Event } _log.Error($"Unable to find event handler list for event type {eventType.FullName}"); } + if (!foundHandler) + _log.Warn($"Found no handlers in {instance.GetType().FullName} or base types"); + } /// diff --git a/Torch/Event/EventShimAttribute.cs b/Torch/Event/EventShimAttribute.cs index 87e5c66..8b2d25e 100644 --- a/Torch/Event/EventShimAttribute.cs +++ b/Torch/Event/EventShimAttribute.cs @@ -14,7 +14,7 @@ namespace Torch.Event /// Event shims should be singleton, and have one (or more) fields that are of type . /// [AttributeUsage(AttributeTargets.Class)] - internal class EventShimAttribute : Attribute + public class EventShimAttribute : Attribute { } }