Kick on ban for entire server group (#28649)
* Start work on PostgresNotificationManager Implement initial version of init and listening code * Finish implementing PostgresNotificationManager Implement ban insert trigger * Implement ignoring notifications if the ban was from the same server * Address reviews * Fixes and refactorings Fix typo in migration SQL Pull new code in BanManager out into its own partial file. Unify logic to kick somebody with that when a new ban is placed directly on the server. New bans are now checked against all parameters (IP, HWID) instead of just user ID. Extracted SQLite ban matching code into a new class so that it can mostly be re-used by the ban notification code. No copy-paste here. Database notifications are now not implicitly sent to the main thread, this means basic checks will happen in the thread pool beforehand. Bans without user ID are now sent to servers. Bans are rate limited to avoid undue work from mass ban imports, beyond the rate limit they are dropped. Improved error handling and logging for the whole system. Matching bans against connected players requires knowing their ban exemption flags. These are now cached when the player connects. ServerBanDef now has exemption flags, again to allow matching full ban details for ban notifications. Made database notifications a proper struct type to reduce copy pasting a tuple. Remove copy pasted connection string building code by just... passing the string into the constructor. Add lock around _notificationHandlers just in case. Fixed postgres connection wait not being called in a loop and therefore spamming LISTEN commands for every received notification. Added more error handling and logging to notification listener. Removed some copy pasting from SQLite database layer too while I was at it because god forbid we expect anybody else to do all the work in this project. Sorry Julian --------- Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
This commit is contained in:
@@ -116,7 +116,7 @@ namespace Content.Server.Database
|
||||
/// Get current ban exemption flags for a user
|
||||
/// </summary>
|
||||
/// <returns><see cref="ServerBanExemptFlags.None"/> if the user is not exempt from any bans.</returns>
|
||||
Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId);
|
||||
Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -304,6 +304,43 @@ namespace Content.Server.Database
|
||||
Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job);
|
||||
|
||||
#endregion
|
||||
|
||||
#region DB Notifications
|
||||
|
||||
void SubscribeToNotifications(Action<DatabaseNotification> handler);
|
||||
|
||||
/// <summary>
|
||||
/// Inject a notification as if it was created by the database. This is intended for testing.
|
||||
/// </summary>
|
||||
/// <param name="notification">The notification to trigger</param>
|
||||
void InjectTestNotification(DatabaseNotification notification);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a notification sent between servers via the database layer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Database notifications are a simple system to broadcast messages to an entire server group
|
||||
/// backed by the same database. For example, this is used to notify all servers of new ban records.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// They are currently implemented by the PostgreSQL <c>NOTIFY</c> and <c>LISTEN</c> commands.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public struct DatabaseNotification
|
||||
{
|
||||
/// <summary>
|
||||
/// The channel for the notification. This can be used to differentiate notifications for different purposes.
|
||||
/// </summary>
|
||||
public required string Channel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The actual contents of the notification. Optional.
|
||||
/// </summary>
|
||||
public string? Payload { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ServerDbManager : IServerDbManager
|
||||
@@ -333,6 +370,8 @@ namespace Content.Server.Database
|
||||
// This is that connection, close it when we shut down.
|
||||
private SqliteConnection? _sqliteInMemoryConnection;
|
||||
|
||||
private readonly List<Action<DatabaseNotification>> _notificationHandlers = [];
|
||||
|
||||
public void Init()
|
||||
{
|
||||
_msLogProvider = new LoggingProvider(_logMgr);
|
||||
@@ -345,6 +384,7 @@ namespace Content.Server.Database
|
||||
|
||||
var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower();
|
||||
var opsLog = _logMgr.GetSawmill("db.op");
|
||||
var notifyLog = _logMgr.GetSawmill("db.notify");
|
||||
switch (engine)
|
||||
{
|
||||
case "sqlite":
|
||||
@@ -352,17 +392,22 @@ namespace Content.Server.Database
|
||||
_db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog);
|
||||
break;
|
||||
case "postgres":
|
||||
var pgOptions = CreatePostgresOptions();
|
||||
_db = new ServerDbPostgres(pgOptions, _cfg, opsLog);
|
||||
var (pgOptions, conString) = CreatePostgresOptions();
|
||||
_db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException($"Unknown database engine {engine}.");
|
||||
}
|
||||
|
||||
_db.OnNotificationReceived += HandleDatabaseNotification;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_db.OnNotificationReceived -= HandleDatabaseNotification;
|
||||
|
||||
_sqliteInMemoryConnection?.Dispose();
|
||||
_db.Shutdown();
|
||||
}
|
||||
|
||||
public Task<PlayerPreferences> InitPrefsAsync(
|
||||
@@ -465,10 +510,10 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.UpdateBanExemption(userId, flags));
|
||||
}
|
||||
|
||||
public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId)
|
||||
public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.GetBanExemption(userId));
|
||||
return RunDbCommand(() => _db.GetBanExemption(userId, cancel));
|
||||
}
|
||||
|
||||
#region Role Ban
|
||||
@@ -806,7 +851,7 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
|
||||
}
|
||||
|
||||
public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
|
||||
public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.GetAllAdminRemarks(player));
|
||||
@@ -907,6 +952,30 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.RemoveJobWhitelist(player, job));
|
||||
}
|
||||
|
||||
public void SubscribeToNotifications(Action<DatabaseNotification> handler)
|
||||
{
|
||||
lock (_notificationHandlers)
|
||||
{
|
||||
_notificationHandlers.Add(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public void InjectTestNotification(DatabaseNotification notification)
|
||||
{
|
||||
HandleDatabaseNotification(notification);
|
||||
}
|
||||
|
||||
private async void HandleDatabaseNotification(DatabaseNotification notification)
|
||||
{
|
||||
lock (_notificationHandlers)
|
||||
{
|
||||
foreach (var handler in _notificationHandlers)
|
||||
{
|
||||
handler(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper functions to run DB commands from the thread pool.
|
||||
// This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
|
||||
// For SQLite, this will also enable read parallelization (within limits).
|
||||
@@ -962,7 +1031,7 @@ namespace Content.Server.Database
|
||||
return enumerable;
|
||||
}
|
||||
|
||||
private DbContextOptions<PostgresServerDbContext> CreatePostgresOptions()
|
||||
private (DbContextOptions<PostgresServerDbContext> options, string connectionString) CreatePostgresOptions()
|
||||
{
|
||||
var host = _cfg.GetCVar(CCVars.DatabasePgHost);
|
||||
var port = _cfg.GetCVar(CCVars.DatabasePgPort);
|
||||
@@ -984,7 +1053,7 @@ namespace Content.Server.Database
|
||||
|
||||
builder.UseNpgsql(connectionString);
|
||||
SetupLogging(builder);
|
||||
return builder.Options;
|
||||
return (builder.Options, connectionString);
|
||||
}
|
||||
|
||||
private void SetupSqlite(out Func<DbContextOptions<SqliteServerDbContext>> contextFunc, out bool inMemory)
|
||||
|
||||
Reference in New Issue
Block a user