Skip to content

Commit

Permalink
Merge remote-tracking branch 'refs/remotes/wizards/master'
Browse files Browse the repository at this point in the history
# Conflicts:
#	Content.Server/Administration/Systems/AdminSystem.cs
#	Content.Server/GameTicking/GameTicker.Spawning.cs
#	Content.Server/IoC/ServerContentIoC.cs
#	Resources/Locale/en-US/_strings/administration/multi-server-kick.ftl
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml
  • Loading branch information
VigersRay committed Jan 22, 2025
2 parents fc3b71f + 20acef1 commit 3c2672e
Show file tree
Hide file tree
Showing 45 changed files with 517 additions and 156 deletions.
29 changes: 3 additions & 26 deletions Content.Server/Administration/Managers/BanManager.Notification.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Database;

namespace Content.Server.Administration.Managers;

Expand Down Expand Up @@ -30,36 +28,15 @@ public sealed partial class BanManager
private TimeSpan _banNotificationRateLimitStart;
private int _banNotificationRateLimitCount;

private void OnDatabaseNotification(DatabaseNotification notification)
private bool OnDatabaseNotificationEarlyFilter()
{
if (notification.Channel != BanNotificationChannel)
return;

if (notification.Payload == null)
{
_sawmill.Error("Got ban notification with null payload!");
return;
}

BanNotificationData data;
try
{
data = JsonSerializer.Deserialize<BanNotificationData>(notification.Payload)
?? throw new JsonException("Content is null");
}
catch (JsonException e)
{
_sawmill.Error($"Got invalid JSON in ban notification: {e}");
return;
}

if (!CheckBanRateLimit())
{
_sawmill.Verbose("Not processing ban notification due to rate limit");
return;
return false;
}

_taskManager.RunOnMainThread(() => ProcessBanNotification(data));
return true;
}

private async void ProcessBanNotification(BanNotificationData data)
Expand Down
7 changes: 6 additions & 1 deletion Content.Server/Administration/Managers/BanManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ public void Initialize()
{
_netManager.RegisterNetMessage<MsgRoleBans>();

_db.SubscribeToNotifications(OnDatabaseNotification);
_db.SubscribeToJsonNotification<BanNotificationData>(
_taskManager,
_sawmill,
BanNotificationChannel,
ProcessBanNotification,
OnDatabaseNotificationEarlyFilter);

_userDbData.AddOnLoadPlayer(CachePlayerData);
_userDbData.AddOnPlayerDisconnect(ClearPlayerData);
Expand Down
114 changes: 114 additions & 0 deletions Content.Server/Administration/Managers/MultiServerKickManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Database;
using Content.Shared.CCVar;
using Robust.Server.Player;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;

namespace Content.Server.Administration.Managers;

/// <summary>
/// Handles kicking people that connect to multiple servers on the same DB at once.
/// </summary>
/// <seealso cref="CCVars.AdminAllowMultiServerPlay"/>
public sealed class MultiServerKickManager
{
public const string NotificationChannel = "multi_server_kick";

[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly IServerDbManager _dbManager = null!;
[Dependency] private readonly ILogManager _logManager = null!;
[Dependency] private readonly IConfigurationManager _cfg = null!;
[Dependency] private readonly IAdminManager _adminManager = null!;
[Dependency] private readonly ITaskManager _taskManager = null!;
[Dependency] private readonly IServerNetManager _netManager = null!;
[Dependency] private readonly ILocalizationManager _loc = null!;
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = null!;

private ISawmill _sawmill = null!;
private bool _allowed;

public void Initialize()
{
_sawmill = _logManager.GetSawmill("multi_server_kick");

_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_cfg.OnValueChanged(CCVars.AdminAllowMultiServerPlay, b => _allowed = b, true);

_dbManager.SubscribeToJsonNotification<NotificationData>(
_taskManager,
_sawmill,
NotificationChannel,
OnNotification,
OnNotificationEarlyFilter
);
}

// ReSharper disable once AsyncVoidMethod
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (_allowed)
return;

if (e.NewStatus != SessionStatus.InGame)
return;

// Send notification to other servers so they can kick this player that just connected.
try
{
await _dbManager.SendNotification(new DatabaseNotification
{
Channel = NotificationChannel,
Payload = JsonSerializer.Serialize(new NotificationData
{
PlayerId = e.Session.UserId,
ServerId = (await _serverDbEntry.ServerEntity).Id,
}),
});
}
catch (Exception ex)
{
_sawmill.Error($"Failed to send notification for multi server kick: {ex}");
}
}

private bool OnNotificationEarlyFilter()
{
if (_allowed)
{
_sawmill.Verbose("Received notification for player join, but multi server play is allowed on this server. Ignoring");
return false;
}

return true;
}

// ReSharper disable once AsyncVoidMethod
private async void OnNotification(NotificationData notification)
{
if (!_playerManager.TryGetSessionById(new NetUserId(notification.PlayerId), out var player))
return;

if (notification.ServerId == (await _serverDbEntry.ServerEntity).Id)
return;

if (_adminManager.IsAdmin(player, includeDeAdmin: true))
return;

_sawmill.Info($"Kicking {player} for connecting to another server. Multi-server play is not allowed.");
_netManager.DisconnectChannel(player.Channel, _loc.GetString("multi-server-kick-reason"));
}

private sealed class NotificationData
{
[JsonPropertyName("player_id")]
public Guid PlayerId { get; set; }

[JsonPropertyName("server_id")]
public int ServerId { get; set; }
}
}
10 changes: 4 additions & 6 deletions Content.Server/Administration/Systems/AdminSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,14 @@ public override void Initialize()
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);

SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);

SubscribeLocalEvent<ActorComponent, EntityRenamedEvent>(OnPlayerRenamed);
SubscribeLocalEvent<ActorComponent, IdentityChangedEvent>(OnIdentityChanged);

IoCManager.Instance!.TryResolveType(out _sponsorsManager); // Sunrise-Sponsors
}
Expand Down Expand Up @@ -149,12 +150,9 @@ public void UpdatePlayerList(ICommonSession player)
return value ?? null;
}

private void OnIdentityChanged(ref IdentityChangedEvent ev)
private void OnIdentityChanged(Entity<ActorComponent> ent, ref IdentityChangedEvent ev)
{
if (!TryComp<ActorComponent>(ev.CharacterEntity, out var actor))
return;

UpdatePlayerList(actor.PlayerSession);
UpdatePlayerList(ent.Comp.PlayerSession);
}

private void OnRoleEvent(RoleEvent ev)
Expand Down
2 changes: 2 additions & 0 deletions Content.Server/Database/ServerDbBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1809,6 +1809,8 @@ await db.DbContext.IPIntelCache

#endregion

public abstract Task SendNotification(DatabaseNotification notification);

// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
// Normalize DateTimes here so they're always Utc. Thanks.
protected abstract DateTime NormalizeDatabaseTime(DateTime time);
Expand Down
15 changes: 15 additions & 0 deletions Content.Server/Database/ServerDbManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,15 @@ Task<int> AddConnectionLogAsync(
/// <param name="notification">The notification to trigger</param>
void InjectTestNotification(DatabaseNotification notification);

/// <summary>
/// Send a notification to all other servers connected to the same database.
/// </summary>
/// <remarks>
/// The local server will receive the sent notification itself again.
/// </remarks>
/// <param name="notification">The notification to send.</param>
Task SendNotification(DatabaseNotification notification);

#endregion
}

Expand Down Expand Up @@ -1045,6 +1054,12 @@ public void InjectTestNotification(DatabaseNotification notification)
HandleDatabaseNotification(notification);
}

public Task SendNotification(DatabaseNotification notification)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SendNotification(notification));
}

private async void HandleDatabaseNotification(DatabaseNotification notification)
{
lock (_notificationHandlers)
Expand Down
76 changes: 76 additions & 0 deletions Content.Server/Database/ServerDbManagerExt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Text.Json;
using Robust.Shared.Asynchronous;

namespace Content.Server.Database;

public static class ServerDbManagerExt
{
/// <summary>
/// Subscribe to a database notification on a specific channel, formatted as JSON.
/// </summary>
/// <param name="dbManager">The database manager to subscribe on.</param>
/// <param name="taskManager">The task manager used to run the main callback on the main thread.</param>
/// <param name="sawmill">Sawmill to log any errors to.</param>
/// <param name="channel">
/// The notification channel to listen on. Only notifications on this channel will be handled.
/// </param>
/// <param name="action">
/// The action to run on the notification data.
/// This runs on the main thread.
/// </param>
/// <param name="earlyFilter">
/// An early filter callback that runs before the JSON message is deserialized.
/// Return false to not handle the notification.
/// This does not run on the main thread.
/// </param>
/// <param name="filter">
/// A filter callback that runs after the JSON message is deserialized.
/// Return false to not handle the notification.
/// This does not run on the main thread.
/// </param>
/// <typeparam name="TData">The type of JSON data to deserialize.</typeparam>
public static void SubscribeToJsonNotification<TData>(
this IServerDbManager dbManager,
ITaskManager taskManager,
ISawmill sawmill,
string channel,
Action<TData> action,
Func<bool>? earlyFilter = null,
Func<TData, bool>? filter = null)
{
dbManager.SubscribeToNotifications(notification =>
{
if (notification.Channel != channel)
return;

if (notification.Payload == null)
{
sawmill.Error($"Got {channel} notification with null payload!");
return;
}

if (earlyFilter != null && !earlyFilter())
return;

TData data;
try
{
data = JsonSerializer.Deserialize<TData>(notification.Payload)
?? throw new JsonException("Content is null");
}
catch (JsonException e)
{
sawmill.Error($"Got invalid JSON in {channel} notification: {e}");
return;
}

if (filter != null && !filter(data))
return;

taskManager.RunOnMainThread(() =>
{
action(data);
});
});
}
}
10 changes: 10 additions & 0 deletions Content.Server/Database/ServerDbPostgres.Notifications.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Managers;
using Microsoft.EntityFrameworkCore;
using Npgsql;

namespace Content.Server.Database;
Expand All @@ -17,6 +18,7 @@ public sealed partial class ServerDbPostgres
private static readonly string[] NotificationChannels =
[
BanManager.BanNotificationChannel,
MultiServerKickManager.NotificationChannel,
];

private static readonly TimeSpan ReconnectWaitIncrease = TimeSpan.FromSeconds(10);
Expand Down Expand Up @@ -111,6 +113,14 @@ private void OnNotification(object _, NpgsqlNotificationEventArgs notification)
});
}

public override async Task SendNotification(DatabaseNotification notification)
{
await using var db = await GetDbImpl();

await db.PgDbContext.Database.ExecuteSqlAsync(
$"SELECT pg_notify({notification.Channel}, {notification.Payload})");
}

public override void Shutdown()
{
_notificationTokenSource.Cancel();
Expand Down
6 changes: 6 additions & 0 deletions Content.Server/Database/ServerDbSqlite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,12 @@ public override async Task<int> AddAdminMessage(AdminMessage message)
return await base.AddAdminMessage(message);
}

public override Task SendNotification(DatabaseNotification notification)
{
// Notifications not implemented on SQLite.
return Task.CompletedTask;
}

protected override DateTime NormalizeDatabaseTime(DateTime time)
{
DebugTools.Assert(time.Kind == DateTimeKind.Unspecified);
Expand Down
1 change: 1 addition & 0 deletions Content.Server/Entry/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ public override void PostInit()
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
IoCManager.Resolve<IBanManager>().Initialize();
IoCManager.Resolve<IConnectionManager>().PostInit();
IoCManager.Resolve<MultiServerKickManager>().Initialize();
}
}

Expand Down
3 changes: 3 additions & 0 deletions Content.Server/GameTicking/GameTicker.Spawning.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Numerics;
using Content.Server._Sunrise.Station;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost;
using Content.Server.Shuttles.Components;
Expand Down Expand Up @@ -34,6 +35,7 @@ public sealed partial class GameTicker
{
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly AdminSystem _admin = default!;
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
[Dependency] private readonly NewLifeSystem _newLifeSystem = default!; // Sunrise-NewLife

Expand Down Expand Up @@ -261,6 +263,7 @@ private void SpawnPlayer(ICommonSession player,

_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
var jobName = _jobs.MindTryGetJobName(newMind);
_admin.UpdatePlayerList(player);

if (lateJoin && !silent)
{
Expand Down
Loading

0 comments on commit 3c2672e

Please sign in to comment.