From d879665b52c6c30d057e94ed49b222b8a1585717 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Tue, 9 Apr 2024 17:25:21 +0200 Subject: [PATCH] Add new "grant_connect_bypass" admin command (#26771) This command allows you to grant a player temporary privilege to join regardless of player cap, whitelist, etc. It does not bypass bans. The API for this is IConnectionManager.AddTemporaryConnectBypass(). I shuffled around the logic inside ConnectionManager. Bans are now checked before panic bunker. --- .../Connection/ConnectionManager.cs | 57 +++++++++++++++--- .../Connection/GrantConnectBypassCommand.cs | 60 +++++++++++++++++++ .../commands/connection-commands.ftl | 16 +++++ 3 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 Content.Server/Connection/GrantConnectBypassCommand.cs create mode 100644 Resources/Locale/en-US/administration/commands/connection-commands.ftl diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index 1367cae82c..cd89f48d49 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Runtime.InteropServices; using System.Text.Json.Nodes; using System.Threading.Tasks; using Content.Server.Database; @@ -10,6 +11,7 @@ using Content.Shared.Players.PlayTimeTracking; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; +using Robust.Shared.Timing; namespace Content.Server.Connection @@ -17,6 +19,18 @@ namespace Content.Server.Connection public interface IConnectionManager { void Initialize(); + + /// + /// Temporarily allow a user to bypass regular connection requirements. + /// + /// + /// The specified user will be allowed to bypass regular player cap, + /// whitelist and panic bunker restrictions for . + /// Bans are not bypassed. + /// + /// The user to give a temporary bypass. + /// How long the bypass should last for. + void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration); } /// @@ -31,15 +45,31 @@ namespace Content.Server.Connection [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly ILocalizationManager _loc = default!; [Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly ILogManager _logManager = default!; + + private readonly Dictionary _temporaryBypasses = []; + private ISawmill _sawmill = default!; public void Initialize() { + _sawmill = _logManager.GetSawmill("connections"); + _netMgr.Connecting += NetMgrOnConnecting; _netMgr.AssignUserIdCallback = AssignUserIdCallback; // Approval-based IP bans disabled because they don't play well with Happy Eyeballs. // _netMgr.HandleApprovalCallback = HandleApproval; } + public void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration) + { + ref var time = ref CollectionsMarshal.GetValueRefOrAddDefault(_temporaryBypasses, user, out _); + var newTime = _gameTiming.RealTime + duration; + // Make sure we only update the time if we wouldn't shrink it. + if (newTime > time) + time = newTime; + } + /* private async Task HandleApproval(NetApprovalEventArgs eventArgs) { @@ -109,6 +139,20 @@ namespace Content.Server.Connection hwId = null; } + var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false); + if (bans.Count > 0) + { + var firstBan = bans[0]; + var message = firstBan.FormatBanMessage(_cfg, _loc); + return (ConnectionDenyReason.Ban, message, bans); + } + + if (HasTemporaryBypass(userId)) + { + _sawmill.Verbose("User {UserId} has temporary bypass, skipping further connection checks", userId); + return null; + } + var adminData = await _dbManager.GetAdminDataForAsync(e.UserId); if (_cfg.GetCVar(CCVars.PanicBunkerEnabled) && adminData == null) @@ -167,14 +211,6 @@ namespace Content.Server.Connection return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null); } - var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false); - if (bans.Count > 0) - { - var firstBan = bans[0]; - var message = firstBan.FormatBanMessage(_cfg, _loc); - return (ConnectionDenyReason.Ban, message, bans); - } - if (_cfg.GetCVar(CCVars.WhitelistEnabled)) { var min = _cfg.GetCVar(CCVars.WhitelistMinPlayers); @@ -195,6 +231,11 @@ namespace Content.Server.Connection return null; } + private bool HasTemporaryBypass(NetUserId user) + { + return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime; + } + private async Task AssignUserIdCallback(string name) { if (!_cfg.GetCVar(CCVars.GamePersistGuests)) diff --git a/Content.Server/Connection/GrantConnectBypassCommand.cs b/Content.Server/Connection/GrantConnectBypassCommand.cs new file mode 100644 index 0000000000..e2d0d7338a --- /dev/null +++ b/Content.Server/Connection/GrantConnectBypassCommand.cs @@ -0,0 +1,60 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Robust.Shared.Console; + +namespace Content.Server.Connection; + +[AdminCommand(AdminFlags.Admin)] +public sealed class GrantConnectBypassCommand : LocalizedCommands +{ + private static readonly TimeSpan DefaultDuration = TimeSpan.FromHours(1); + + [Dependency] private readonly IPlayerLocator _playerLocator = default!; + [Dependency] private readonly IConnectionManager _connectionManager = default!; + + public override string Command => "grant_connect_bypass"; + + public override async void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length is not (1 or 2)) + { + shell.WriteError(Loc.GetString("cmd-grant_connect_bypass-invalid-args")); + return; + } + + var argPlayer = args[0]; + var info = await _playerLocator.LookupIdByNameOrIdAsync(argPlayer); + if (info == null) + { + shell.WriteError(Loc.GetString("cmd-grant_connect_bypass-unknown-user", ("user", argPlayer))); + return; + } + + var duration = DefaultDuration; + if (args.Length > 1) + { + var argDuration = args[2]; + if (!uint.TryParse(argDuration, out var minutes)) + { + shell.WriteLine(Loc.GetString("cmd-grant_connect_bypass-invalid-duration", ("duration", argDuration))); + return; + } + + duration = TimeSpan.FromMinutes(minutes); + } + + _connectionManager.AddTemporaryConnectBypass(info.UserId, duration); + shell.WriteLine(Loc.GetString("cmd-grant_connect_bypass-success", ("user", argPlayer))); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + return CompletionResult.FromHint(Loc.GetString("cmd-grant_connect_bypass-arg-user")); + + if (args.Length == 2) + return CompletionResult.FromHint(Loc.GetString("cmd-grant_connect_bypass-arg-duration")); + + return CompletionResult.Empty; + } +} diff --git a/Resources/Locale/en-US/administration/commands/connection-commands.ftl b/Resources/Locale/en-US/administration/commands/connection-commands.ftl new file mode 100644 index 0000000000..66991042d2 --- /dev/null +++ b/Resources/Locale/en-US/administration/commands/connection-commands.ftl @@ -0,0 +1,16 @@ +## Strings for the "grant_connect_bypass" command. + +cmd-grant_connect_bypass-desc = Temporarily allow a user to bypass regular connection checks. +cmd-grant_connect_bypass-help = Usage: grant_connect_bypass [duration minutes] + Temporarily grants a user the ability to bypass regular connections restrictions. + The bypass only applies to this game server and will expire after (by default) 1 hour. + They will be able to join regardless of whitelist, panic bunker, or player cap. + +cmd-grant_connect_bypass-arg-user = +cmd-grant_connect_bypass-arg-duration = [duration minutes] + +cmd-grant_connect_bypass-invalid-args = Expected 1 or 2 arguments +cmd-grant_connect_bypass-unknown-user = Unable to find user '{$user}' +cmd-grant_connect_bypass-invalid-duration = Invalid duration '{$duration}' + +cmd-grant_connect_bypass-success = Successfully added bypass for user '{$user}' -- 2.51.2