* 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>
124 lines
4.2 KiB
C#
124 lines
4.2 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Content.Server.Database;
|
|
|
|
namespace Content.Server.Administration.Managers;
|
|
|
|
public sealed partial class BanManager
|
|
{
|
|
// Responsible for ban notification handling.
|
|
// Ban notifications are sent through the database to notify the entire server group that a new ban has been added,
|
|
// so that people will get kicked if they are banned on a different server than the one that placed the ban.
|
|
//
|
|
// Ban notifications are currently sent by a trigger in the database, automatically.
|
|
|
|
/// <summary>
|
|
/// The notification channel used to broadcast information about new bans.
|
|
/// </summary>
|
|
public const string BanNotificationChannel = "ban_notification";
|
|
|
|
// Rate limit to avoid undue load from mass-ban imports.
|
|
// Only process 10 bans per 30 second interval.
|
|
//
|
|
// I had the idea of maybe binning this by postgres transaction ID,
|
|
// to avoid any possibility of dropping a normal ban by coincidence.
|
|
// Didn't bother implementing this though.
|
|
private static readonly TimeSpan BanNotificationRateLimitTime = TimeSpan.FromSeconds(30);
|
|
private const int BanNotificationRateLimitCount = 10;
|
|
|
|
private readonly object _banNotificationRateLimitStateLock = new();
|
|
private TimeSpan _banNotificationRateLimitStart;
|
|
private int _banNotificationRateLimitCount;
|
|
|
|
private void OnDatabaseNotification(DatabaseNotification notification)
|
|
{
|
|
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;
|
|
}
|
|
|
|
_taskManager.RunOnMainThread(() => ProcessBanNotification(data));
|
|
}
|
|
|
|
private async void ProcessBanNotification(BanNotificationData data)
|
|
{
|
|
if ((await _entryManager.ServerEntity).Id == data.ServerId)
|
|
{
|
|
_sawmill.Verbose("Not processing ban notification: came from this server");
|
|
return;
|
|
}
|
|
|
|
_sawmill.Verbose($"Processing ban notification for ban {data.BanId}");
|
|
var ban = await _db.GetServerBanAsync(data.BanId);
|
|
if (ban == null)
|
|
{
|
|
_sawmill.Warning($"Ban in notification ({data.BanId}) didn't exist?");
|
|
return;
|
|
}
|
|
|
|
KickMatchingConnectedPlayers(ban, "ban notification");
|
|
}
|
|
|
|
private bool CheckBanRateLimit()
|
|
{
|
|
lock (_banNotificationRateLimitStateLock)
|
|
{
|
|
var now = _gameTiming.RealTime;
|
|
if (_banNotificationRateLimitStart + BanNotificationRateLimitTime < now)
|
|
{
|
|
// Rate limit period expired, restart it.
|
|
_banNotificationRateLimitCount = 1;
|
|
_banNotificationRateLimitStart = now;
|
|
return true;
|
|
}
|
|
|
|
_banNotificationRateLimitCount += 1;
|
|
return _banNotificationRateLimitCount <= BanNotificationRateLimitCount;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Data sent along the notification channel for a single ban notification.
|
|
/// </summary>
|
|
private sealed class BanNotificationData
|
|
{
|
|
/// <summary>
|
|
/// The ID of the new ban object in the database to check.
|
|
/// </summary>
|
|
[JsonRequired, JsonPropertyName("ban_id")]
|
|
public int BanId { get; init; }
|
|
|
|
/// <summary>
|
|
/// The id of the server the ban was made on.
|
|
/// This is used to avoid double work checking the ban on the originating server.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is optional in case the ban was made outside a server (SS14.Admin)
|
|
/// </remarks>
|
|
[JsonPropertyName("server_id")]
|
|
public int? ServerId { get; init; }
|
|
}
|
|
}
|