* Throttle people trying to connect to a full server. To reduce spam/load on the server and connection logs table. Players are forced to wait 30 seconds after getting denied for "server full", before they can try connecting again. This code is an absolute nightmare mess. I tried to re-use the existing code for the redial timer but god everything here sucks so much. Requires https://github.com/space-wizards/RobustToolbox/pull/4487 * Use new NetDisconnectMessage API instead. * Add admin.bypass_max_players CVar. Just something to help with debugging the player cap on dev, I don't expect this to ever be disabled on real servers.
217 lines
9.2 KiB
C#
217 lines
9.2 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text.Json.Nodes;
|
|
using System.Threading.Tasks;
|
|
using Content.Server.Database;
|
|
using Content.Server.GameTicking;
|
|
using Content.Server.Preferences.Managers;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.GameTicking;
|
|
using Content.Shared.Players.PlayTimeTracking;
|
|
using Robust.Server.Player;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.Network;
|
|
|
|
|
|
namespace Content.Server.Connection
|
|
{
|
|
public interface IConnectionManager
|
|
{
|
|
void Initialize();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles various duties like guest username assignment, bans, connection logs, etc...
|
|
/// </summary>
|
|
public sealed class ConnectionManager : IConnectionManager
|
|
{
|
|
[Dependency] private readonly IServerDbManager _dbManager = default!;
|
|
[Dependency] private readonly IPlayerManager _plyMgr = default!;
|
|
[Dependency] private readonly IServerNetManager _netMgr = default!;
|
|
[Dependency] private readonly IServerDbManager _db = default!;
|
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
|
[Dependency] private readonly ILocalizationManager _loc = default!;
|
|
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!;
|
|
|
|
public void Initialize()
|
|
{
|
|
_netMgr.Connecting += NetMgrOnConnecting;
|
|
_netMgr.AssignUserIdCallback = AssignUserIdCallback;
|
|
// Approval-based IP bans disabled because they don't play well with Happy Eyeballs.
|
|
// _netMgr.HandleApprovalCallback = HandleApproval;
|
|
}
|
|
|
|
/*
|
|
private async Task<NetApproval> HandleApproval(NetApprovalEventArgs eventArgs)
|
|
{
|
|
var ban = await _db.GetServerBanByIpAsync(eventArgs.Connection.RemoteEndPoint.Address);
|
|
if (ban != null)
|
|
{
|
|
var expires = Loc.GetString("ban-banned-permanent");
|
|
if (ban.ExpirationTime is { } expireTime)
|
|
{
|
|
var duration = expireTime - ban.BanTime;
|
|
var utc = expireTime.ToUniversalTime();
|
|
expires = Loc.GetString("ban-expires", ("duration", duration.TotalMinutes.ToString("N0")), ("time", utc.ToString("f")));
|
|
}
|
|
var reason = Loc.GetString("ban-banned-1") + "\n" + Loc.GetString("ban-banned-2", ("reason", this.Reason)) + "\n" + expires;;
|
|
return NetApproval.Deny(reason);
|
|
}
|
|
|
|
return NetApproval.Allow();
|
|
}
|
|
*/
|
|
|
|
private async Task NetMgrOnConnecting(NetConnectingArgs e)
|
|
{
|
|
var deny = await ShouldDeny(e);
|
|
|
|
var addr = e.IP.Address;
|
|
var userId = e.UserId;
|
|
|
|
var serverId = (await _serverDbEntry.ServerEntity).Id;
|
|
|
|
if (deny != null)
|
|
{
|
|
var (reason, msg, banHits) = deny.Value;
|
|
|
|
var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, reason, serverId);
|
|
if (banHits is { Count: > 0 })
|
|
await _db.AddServerBanHitsAsync(id, banHits);
|
|
|
|
var properties = new Dictionary<string, object>();
|
|
if (reason == ConnectionDenyReason.Full)
|
|
properties["delay"] = _cfg.GetCVar(CCVars.GameServerFullReconnectDelay);
|
|
|
|
e.Deny(new NetDenyReason(msg, properties));
|
|
}
|
|
else
|
|
{
|
|
await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, null, serverId);
|
|
|
|
if (!ServerPreferencesManager.ShouldStorePrefs(e.AuthType))
|
|
return;
|
|
|
|
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, e.UserData.HWId);
|
|
}
|
|
}
|
|
|
|
private async Task<(ConnectionDenyReason, string, List<ServerBanDef>? bansHit)?> ShouldDeny(
|
|
NetConnectingArgs e)
|
|
{
|
|
// Check if banned.
|
|
var addr = e.IP.Address;
|
|
var userId = e.UserId;
|
|
ImmutableArray<byte>? hwId = e.UserData.HWId;
|
|
if (hwId.Value.Length == 0 || !_cfg.GetCVar(CCVars.BanHardwareIds))
|
|
{
|
|
// HWId not available for user's platform, don't look it up.
|
|
// Or hardware ID checks disabled.
|
|
hwId = null;
|
|
}
|
|
|
|
var adminData = await _dbManager.GetAdminDataForAsync(e.UserId);
|
|
|
|
if (_cfg.GetCVar(CCVars.PanicBunkerEnabled) && adminData == null)
|
|
{
|
|
var showReason = _cfg.GetCVar(CCVars.PanicBunkerShowReason);
|
|
var customReason = _cfg.GetCVar(CCVars.PanicBunkerCustomReason);
|
|
|
|
var minMinutesAge = _cfg.GetCVar(CCVars.PanicBunkerMinAccountAge);
|
|
var record = await _dbManager.GetPlayerRecordByUserId(userId);
|
|
var validAccountAge = record != null &&
|
|
record.FirstSeenTime.CompareTo(DateTimeOffset.Now - TimeSpan.FromMinutes(minMinutesAge)) <= 0;
|
|
var bypassAllowed = _cfg.GetCVar(CCVars.BypassBunkerWhitelist) && await _db.GetWhitelistStatusAsync(userId);
|
|
|
|
// Use the custom reason if it exists & they don't have the minimum account age
|
|
if (customReason != string.Empty && !validAccountAge && !bypassAllowed)
|
|
{
|
|
return (ConnectionDenyReason.Panic, customReason, null);
|
|
}
|
|
|
|
if (showReason && !validAccountAge && !bypassAllowed)
|
|
{
|
|
return (ConnectionDenyReason.Panic,
|
|
Loc.GetString("panic-bunker-account-denied-reason",
|
|
("reason", Loc.GetString("panic-bunker-account-reason-account", ("minutes", minMinutesAge)))), null);
|
|
}
|
|
|
|
var minOverallHours = _cfg.GetCVar(CCVars.PanicBunkerMinOverallHours);
|
|
var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
|
|
var haveMinOverallTime = overallTime != null && overallTime.TimeSpent.TotalHours > minOverallHours;
|
|
|
|
// Use the custom reason if it exists & they don't have the minimum time
|
|
if (customReason != string.Empty && !haveMinOverallTime && !bypassAllowed)
|
|
{
|
|
return (ConnectionDenyReason.Panic, customReason, null);
|
|
}
|
|
|
|
if (showReason && !haveMinOverallTime && !bypassAllowed)
|
|
{
|
|
return (ConnectionDenyReason.Panic,
|
|
Loc.GetString("panic-bunker-account-denied-reason",
|
|
("reason", Loc.GetString("panic-bunker-account-reason-overall", ("hours", minOverallHours)))), null);
|
|
}
|
|
|
|
if (!validAccountAge || !haveMinOverallTime && !bypassAllowed)
|
|
{
|
|
return (ConnectionDenyReason.Panic, Loc.GetString("panic-bunker-account-denied"), null);
|
|
}
|
|
}
|
|
|
|
var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
|
|
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
|
|
status == PlayerGameStatus.JoinedGame;
|
|
var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && adminData != null;
|
|
if ((_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !adminBypass) && !wasInGame)
|
|
{
|
|
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);
|
|
var max = _cfg.GetCVar(CCVars.WhitelistMaxPlayers);
|
|
var playerCountValid = _plyMgr.PlayerCount >= min && _plyMgr.PlayerCount < max;
|
|
|
|
if (playerCountValid && await _db.GetWhitelistStatusAsync(userId) == false
|
|
&& adminData is null)
|
|
{
|
|
var msg = Loc.GetString(_cfg.GetCVar(CCVars.WhitelistReason));
|
|
// was the whitelist playercount changed?
|
|
if (min > 0 || max < int.MaxValue)
|
|
msg += "\n" + Loc.GetString("whitelist-playercount-invalid", ("min", min), ("max", max));
|
|
return (ConnectionDenyReason.Whitelist, msg, null);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async Task<NetUserId?> AssignUserIdCallback(string name)
|
|
{
|
|
if (!_cfg.GetCVar(CCVars.GamePersistGuests))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var userId = await _db.GetAssignedUserIdAsync(name);
|
|
if (userId != null)
|
|
{
|
|
return userId;
|
|
}
|
|
|
|
var assigned = new NetUserId(Guid.NewGuid());
|
|
await _db.AssignUserIdAsync(name, assigned);
|
|
return assigned;
|
|
}
|
|
}
|
|
}
|