Files
OldThink/Content.Server/Database/ServerDbPostgres.cs

589 lines
19 KiB
C#
Raw Normal View History

using System.Collections.Immutable;
2020-11-10 16:50:28 +01:00
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
2023-12-10 16:30:12 +01:00
using System.Runtime.CompilerServices;
2020-11-10 16:50:28 +01:00
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server._White;
using Content.Server.IP;
using Content.Shared.CCVar;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
2023-04-03 02:24:55 +02:00
using Robust.Shared.Utility;
namespace Content.Server.Database
{
public sealed class ServerDbPostgres : ServerDbBase
{
private readonly DbContextOptions<PostgresServerDbContext> _options;
private readonly SemaphoreSlim _prefsSemaphore;
private readonly Task _dbReadyTask;
2023-12-10 16:30:12 +01:00
private int _msLag;
public ServerDbPostgres(
DbContextOptions<PostgresServerDbContext> options,
IConfigurationManager cfg,
ISawmill opsLog)
: base(opsLog)
{
var concurrency = cfg.GetCVar(CCVars.DatabasePgConcurrency);
_options = options;
_prefsSemaphore = new SemaphoreSlim(concurrency, concurrency);
_dbReadyTask = Task.Run(async () =>
{
await using var ctx = new PostgresServerDbContext(_options);
try
{
await ctx.Database.MigrateAsync();
}
finally
{
await ctx.DisposeAsync();
}
});
2023-12-10 16:30:12 +01:00
cfg.OnValueChanged(CCVars.DatabasePgFakeLag, v => _msLag = v, true);
}
2022-02-21 14:11:39 -08:00
#region Ban
public override async Task<ServerBanDef?> GetServerBanAsync(int id)
{
await using var db = await GetDbImpl();
var query = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(p => p.Id == id);
var ban = await query.SingleOrDefaultAsync();
return ConvertBan(ban);
}
2021-03-22 01:30:50 +01:00
public override async Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
string serverName = GlobalServerName)
{
2022-02-21 14:11:39 -08:00
if (address == null && userId == null && hwId == null)
{
2022-02-21 14:11:39 -08:00
throw new ArgumentException("Address, userId, and hwId cannot all be null");
}
await using var db = await GetDbImpl();
2023-04-03 02:24:55 +02:00
var exempt = await GetBanExemptionCore(db, userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt, serverName)
2021-03-22 01:30:50 +01:00
.OrderByDescending(b => b.BanTime);
var ban = await query.FirstOrDefaultAsync();
return ConvertBan(ban);
}
public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? address,
2021-03-22 01:30:50 +01:00
NetUserId? userId,
ImmutableArray<byte>? hwId, bool includeUnbanned, string serverName = GlobalServerName)
{
2021-03-22 01:30:50 +01:00
if (address == null && userId == null && hwId == null)
{
2022-02-21 14:11:39 -08:00
throw new ArgumentException("Address, userId, and hwId cannot all be null");
}
await using var db = await GetDbImpl();
2023-04-03 02:24:55 +02:00
var exempt = await GetBanExemptionCore(db, userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt, serverName);
var queryBans = await query.ToArrayAsync();
2021-03-22 01:30:50 +01:00
var bans = new List<ServerBanDef>(queryBans.Length);
foreach (var ban in queryBans)
{
var banDef = ConvertBan(ban);
if (banDef != null)
{
bans.Add(banDef);
}
}
return bans;
}
private static IQueryable<ServerBan> MakeBanLookupQuery(
2021-03-22 01:30:50 +01:00
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
DbGuardImpl db,
2023-04-03 02:24:55 +02:00
bool includeUnbanned,
ServerBanExemptFlags? exemptFlags,
string serverName = GlobalServerName)
2021-03-22 01:30:50 +01:00
{
2023-04-03 02:24:55 +02:00
DebugTools.Assert(!(address == null && userId == null && hwId == null));
IQueryable<ServerBan>? query = null;
2021-03-22 01:30:50 +01:00
if (userId is { } uid)
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
2021-03-22 01:30:50 +01:00
query = query == null ? newQ : query.Union(newQ);
}
2023-04-27 13:59:18 -05:00
if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP))
2021-03-22 01:30:50 +01:00
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address));
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
2021-03-22 01:30:50 +01:00
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
2021-03-22 10:55:49 +01:00
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
2021-03-22 01:30:50 +01:00
query = query == null ? newQ : query.Union(newQ);
}
2023-04-03 02:24:55 +02:00
DebugTools.Assert(
query != null,
"At least one filter item (IP/UserID/HWID) must have been given to make query not null.");
query = query.Where(p =>
p.ServerName == serverName || p.ServerName == "unknown" || string.IsNullOrEmpty(p.ServerName) || serverName == GlobalServerName);
if (!includeUnbanned)
{
2023-04-03 02:24:55 +02:00
query = query.Where(p =>
p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now));
}
2023-04-03 02:24:55 +02:00
if (exemptFlags is { } exempt)
{
query = query.Where(b => (b.ExemptFlags & exempt) == 0);
}
return query.Distinct();
2021-03-22 01:30:50 +01:00
}
private static ServerBanDef? ConvertBan(ServerBan? ban)
{
if (ban == null)
{
return null;
}
NetUserId? uid = null;
if (ban.PlayerUserId is {} guid)
{
uid = new NetUserId(guid);
}
NetUserId? aUid = null;
if (ban.BanningAdmin is {} aGuid)
{
aUid = new NetUserId(aGuid);
}
var unbanDef = ConvertUnban(ban.Unban);
return new ServerBanDef(
ban.Id,
uid,
ban.Address.ToTuple(),
2021-03-22 01:30:50 +01:00
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
ban.PlaytimeAtNote,
ban.Reason,
ban.Severity,
aUid,
unbanDef,
ban.ServerName ??= "unknown");
}
private static ServerUnbanDef? ConvertUnban(ServerUnban? unban)
{
if (unban == null)
{
return null;
}
NetUserId? aUid = null;
if (unban.UnbanningAdmin is {} aGuid)
{
aUid = new NetUserId(aGuid);
}
return new ServerUnbanDef(
unban.Id,
aUid,
unban.UnbanTime);
}
public override async Task AddServerBanAsync(ServerBanDef serverBan)
{
await using var db = await GetDbImpl();
db.PgDbContext.Ban.Add(new ServerBan
{
Address = serverBan.Address.ToNpgsqlInet(),
2021-03-22 01:30:50 +01:00
HWId = serverBan.HWId?.ToArray(),
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
PlaytimeAtNote = serverBan.PlaytimeAtNote,
PlayerUserId = serverBan.UserId?.UserId,
ServerName = serverBan.ServerName
});
await db.PgDbContext.SaveChangesAsync();
}
public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban)
{
await using var db = await GetDbImpl();
db.PgDbContext.Unban.Add(new ServerUnban
{
2022-02-21 21:00:55 -08:00
BanId = serverUnban.BanId,
UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId,
UnbanTime = serverUnban.UnbanTime.UtcDateTime
});
await db.PgDbContext.SaveChangesAsync();
}
2022-02-21 14:11:39 -08:00
#endregion
#region Role Ban
public override async Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
await using var db = await GetDbImpl();
var query = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(p => p.Id == id);
var ban = await query.SingleOrDefaultAsync();
return ConvertRoleBan(ban);
}
public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned,
string serverName = GlobalServerName)
2022-02-21 14:11:39 -08:00
{
if (address == null && userId == null && hwId == null)
{
throw new ArgumentException("Address, userId, and hwId cannot all be null");
}
await using var db = await GetDbImpl();
var query = MakeRoleBanLookupQuery(address, userId, hwId, db, includeUnbanned, serverName)
2022-02-21 14:11:39 -08:00
.OrderByDescending(b => b.BanTime);
return await QueryRoleBans(query);
}
private static async Task<List<ServerRoleBanDef>> QueryRoleBans(IQueryable<ServerRoleBan> query)
{
var queryRoleBans = await query.ToArrayAsync();
var bans = new List<ServerRoleBanDef>(queryRoleBans.Length);
foreach (var ban in queryRoleBans)
{
var banDef = ConvertRoleBan(ban);
if (banDef != null)
{
bans.Add(banDef);
}
}
return bans;
}
private static IQueryable<ServerRoleBan> MakeRoleBanLookupQuery(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
DbGuardImpl db,
bool includeUnbanned,
string serverName = GlobalServerName)
2022-02-21 14:11:39 -08:00
{
IQueryable<ServerRoleBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
2022-02-21 14:11:39 -08:00
query = query == null ? newQ : query.Union(newQ);
}
if (address != null)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address));
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
2022-02-21 14:11:39 -08:00
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
query = query?.Where(p =>
p.ServerName == serverName || p.ServerName == "unknown" || string.IsNullOrEmpty(p.ServerName) || serverName == GlobalServerName);
2022-02-21 14:11:39 -08:00
if (!includeUnbanned)
{
query = query?.Where(p =>
p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now));
}
query = query!.Distinct();
return query;
}
[return: NotNullIfNotNull(nameof(ban))]
2022-02-21 14:11:39 -08:00
private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban)
{
if (ban == null)
{
return null;
}
NetUserId? uid = null;
if (ban.PlayerUserId is {} guid)
2022-02-21 14:11:39 -08:00
{
uid = new NetUserId(guid);
}
NetUserId? aUid = null;
if (ban.BanningAdmin is {} aGuid)
{
aUid = new NetUserId(aGuid);
}
var unbanDef = ConvertRoleUnban(ban.Unban);
return new ServerRoleBanDef(
ban.Id,
uid,
ban.Address.ToTuple(),
2022-02-21 14:11:39 -08:00
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
ban.PlaytimeAtNote,
2022-02-21 14:11:39 -08:00
ban.Reason,
ban.Severity,
2022-02-21 14:11:39 -08:00
aUid,
unbanDef,
ban.RoleId,
ban.ServerName ??= "unknown");
2022-02-21 14:11:39 -08:00
}
private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban)
{
if (unban == null)
{
return null;
}
NetUserId? aUid = null;
if (unban.UnbanningAdmin is {} aGuid)
{
aUid = new NetUserId(aGuid);
}
return new ServerRoleUnbanDef(
unban.Id,
aUid,
unban.UnbanTime);
}
public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
2022-02-21 14:11:39 -08:00
{
await using var db = await GetDbImpl();
var ban = new ServerRoleBan
2022-02-21 14:11:39 -08:00
{
Address = serverRoleBan.Address.ToNpgsqlInet(),
2022-02-21 14:11:39 -08:00
HWId = serverRoleBan.HWId?.ToArray(),
Reason = serverRoleBan.Reason,
Severity = serverRoleBan.Severity,
2022-02-21 14:11:39 -08:00
BanningAdmin = serverRoleBan.BanningAdmin?.UserId,
BanTime = serverRoleBan.BanTime.UtcDateTime,
ExpirationTime = serverRoleBan.ExpirationTime?.UtcDateTime,
RoundId = serverRoleBan.RoundId,
PlaytimeAtNote = serverRoleBan.PlaytimeAtNote,
PlayerUserId = serverRoleBan.UserId?.UserId,
2022-02-21 14:11:39 -08:00
RoleId = serverRoleBan.Role,
ServerName = serverRoleBan.ServerName
};
db.PgDbContext.RoleBan.Add(ban);
2022-02-21 14:11:39 -08:00
await db.PgDbContext.SaveChangesAsync();
return ConvertRoleBan(ban);
2022-02-21 14:11:39 -08:00
}
public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban)
{
await using var db = await GetDbImpl();
db.PgDbContext.RoleUnban.Add(new ServerRoleUnban
{
2022-02-21 21:00:55 -08:00
BanId = serverRoleUnban.BanId,
UnbanningAdmin = serverRoleUnban.UnbanningAdmin?.UserId,
UnbanTime = serverRoleUnban.UnbanTime.UtcDateTime
2022-02-21 14:11:39 -08:00
});
await db.PgDbContext.SaveChangesAsync();
}
#endregion
2021-11-11 17:54:02 +01:00
protected override PlayerRecord MakePlayerRecord(Player record)
2020-11-10 16:50:28 +01:00
{
return new PlayerRecord(
new NetUserId(record.UserId),
2020-11-13 03:23:13 +01:00
new DateTimeOffset(record.FirstSeenTime),
2020-11-10 16:50:28 +01:00
record.LastSeenUserName,
2020-11-13 03:23:13 +01:00
new DateTimeOffset(record.LastSeenTime),
2021-03-22 01:30:50 +01:00
record.LastSeenAddress,
record.LastSeenHWId?.ToImmutableArray());
2020-11-10 16:50:28 +01:00
}
public override async Task<int> AddConnectionLogAsync(
2021-03-22 01:30:50 +01:00
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ConnectionDenyReason? denied,
int serverId)
{
await using var db = await GetDbImpl();
var connectionLog = new ConnectionLog
{
Address = address,
Time = DateTime.UtcNow,
UserId = userId.UserId,
2021-03-22 01:30:50 +01:00
UserName = userName,
HWId = hwId.ToArray(),
Denied = denied,
ServerId = serverId
};
db.PgDbContext.ConnectionLog.Add(connectionLog);
await db.PgDbContext.SaveChangesAsync();
return connectionLog.Id;
}
2020-11-10 16:50:28 +01:00
public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])>
GetAllAdminAndRanksAsync(CancellationToken cancel)
{
await using var db = await GetDbImpl();
// Honestly this probably doesn't even matter but whatever.
await using var tx =
await db.DbContext.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancel);
// Join with the player table to find their last seen username, if they have one.
var admins = await db.PgDbContext.Admin
.Include(a => a.Flags)
.GroupJoin(db.PgDbContext.Player, a => a.UserId, p => p.UserId, (a, grouping) => new {a, grouping})
2020-11-13 01:51:42 +01:00
.SelectMany(t => t.grouping.DefaultIfEmpty(), (t, p) => new {t.a, p!.LastSeenUserName})
2020-11-10 16:50:28 +01:00
.ToArrayAsync(cancel);
var adminRanks = await db.DbContext.AdminRank.Include(a => a.Flags).ToArrayAsync(cancel);
return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!;
}
protected override IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null)
{
// https://learn.microsoft.com/en-us/ef/core/querying/sql-queries#passing-parameters
// Read the link above for parameterization before changing this method or you get the bullet
if (!string.IsNullOrWhiteSpace(filter?.Search))
{
return db.AdminLog.FromSql($"""
SELECT a.admin_log_id, a.round_id, a.date, a.impact, a.json, a.message, a.type FROM admin_log AS a
WHERE to_tsvector('english'::regconfig, a.message) @@ websearch_to_tsquery('english'::regconfig, {filter.Search})
""");
}
return db.AdminLog;
}
2023-12-10 16:30:12 +01:00
private async Task<DbGuardImpl> GetDbImpl([CallerMemberName] string? name = null)
{
2023-12-10 16:30:12 +01:00
LogDbOp(name);
await _dbReadyTask;
await _prefsSemaphore.WaitAsync();
2023-12-10 16:30:12 +01:00
if (_msLag > 0)
await Task.Delay(_msLag);
return new DbGuardImpl(this, new PostgresServerDbContext(_options));
}
2023-12-10 16:30:12 +01:00
protected override async Task<DbGuard> GetDb([CallerMemberName] string? name = null)
{
2023-12-10 16:30:12 +01:00
return await GetDbImpl(name);
}
private sealed class DbGuardImpl : DbGuard
{
private readonly ServerDbPostgres _db;
public DbGuardImpl(ServerDbPostgres db, PostgresServerDbContext dbC)
{
_db = db;
PgDbContext = dbC;
}
public PostgresServerDbContext PgDbContext { get; }
public override ServerDbContext DbContext => PgDbContext;
public override async ValueTask DisposeAsync()
{
await DbContext.DisposeAsync();
_db._prefsSemaphore.Release();
}
}
}
}