using System.Collections.Immutable;
+using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Content.Server.Database;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
+using Robust.Shared.Timing;
namespace Content.Server.Connection
public interface IConnectionManager
{
void Initialize();
+
+ /// <summary>
+ /// Temporarily allow a user to bypass regular connection requirements.
+ /// </summary>
+ /// <remarks>
+ /// The specified user will be allowed to bypass regular player cap,
+ /// whitelist and panic bunker restrictions for <paramref name="duration"/>.
+ /// Bans are not bypassed.
+ /// </remarks>
+ /// <param name="user">The user to give a temporary bypass.</param>
+ /// <param name="duration">How long the bypass should last for.</param>
+ void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration);
}
/// <summary>
[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<NetUserId, TimeSpan> _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<NetApproval> HandleApproval(NetApprovalEventArgs eventArgs)
{
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)
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);
return null;
}
+ private bool HasTemporaryBypass(NetUserId user)
+ {
+ return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime;
+ }
+
private async Task<NetUserId?> AssignUserIdCallback(string name)
{
if (!_cfg.GetCVar(CCVars.GamePersistGuests))
--- /dev/null
+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;
+ }
+}
--- /dev/null
+## 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 <user> [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 = <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}'