Holy crap auth works (#2099)
* Holy crap auth works * Fix some usages of UserID instead of UserName * Refactor preferences. They be non-async now. Also faster. * Rename DbContext. * Guest username assignment. * Fix saving of profiles. * Don't store data for guests. * Fix generating invalid random colors. * Don't allow dumb garbage for char preferences. * Bans. * Lol forgot to fill out the command description. * Connection log. * Rename all the tables and columns to be snake_case. * Re-do migrations. * Fixing tests and warnings. * Update submodule
This commit is contained in:
committed by
GitHub
parent
8a33e0a9bd
commit
66c8a68891
41
Content.Server/Database/ServerBanDef.cs
Normal file
41
Content.Server/Database/ServerBanDef.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Server.Database
|
||||
{
|
||||
public sealed class ServerBanDef
|
||||
{
|
||||
public NetUserId? UserId { get; }
|
||||
public (IPAddress address, int cidrMask)? Address { get; }
|
||||
|
||||
public DateTimeOffset BanTime { get; }
|
||||
public DateTimeOffset? ExpirationTime { get; }
|
||||
public string Reason { get; }
|
||||
public NetUserId? BanningAdmin { get; }
|
||||
|
||||
public ServerBanDef(NetUserId? userId, (IPAddress, int)? address, DateTimeOffset banTime, DateTimeOffset? expirationTime, string reason, NetUserId? banningAdmin)
|
||||
{
|
||||
if (userId == null && address == null)
|
||||
{
|
||||
throw new ArgumentException("Must have a banned user, banned address, or both.");
|
||||
}
|
||||
|
||||
if (address is {} addr && addr.Item1.IsIPv4MappedToIPv6)
|
||||
{
|
||||
// Fix IPv6-mapped IPv4 addresses
|
||||
// So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes.
|
||||
address = (addr.Item1.MapToIPv4(), addr.Item2 - 96);
|
||||
}
|
||||
|
||||
UserId = userId;
|
||||
Address = address;
|
||||
BanTime = banTime;
|
||||
ExpirationTime = expirationTime;
|
||||
Reason = reason;
|
||||
BanningAdmin = banningAdmin;
|
||||
}
|
||||
}
|
||||
}
|
||||
221
Content.Server/Database/ServerDbBase.cs
Normal file
221
Content.Server/Database/ServerDbBase.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Preferences;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Database
|
||||
{
|
||||
public abstract class ServerDbBase
|
||||
{
|
||||
public async Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
var prefs = await db.DbContext
|
||||
.Preference
|
||||
.Include(p => p.Profiles).ThenInclude(h => h.Jobs)
|
||||
.Include(p => p.Profiles).ThenInclude(h => h.Antags)
|
||||
.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
|
||||
|
||||
if (prefs is null) return null;
|
||||
|
||||
var maxSlot = prefs.Profiles.Max(p => p.Slot)+1;
|
||||
var profiles = new ICharacterProfile[maxSlot];
|
||||
foreach (var profile in prefs.Profiles)
|
||||
{
|
||||
profiles[profile.Slot] = ConvertProfiles(profile);
|
||||
}
|
||||
|
||||
return new PlayerPreferences
|
||||
(
|
||||
profiles,
|
||||
prefs.SelectedCharacterSlot
|
||||
);
|
||||
}
|
||||
|
||||
public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
var prefs = await db.DbContext.Preference.SingleAsync(p => p.UserId == userId.UserId);
|
||||
prefs.SelectedCharacterSlot = index;
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot)
|
||||
{
|
||||
if (profile is null)
|
||||
{
|
||||
await DeleteCharacterSlotAsync(userId, slot);
|
||||
return;
|
||||
}
|
||||
|
||||
await using var db = await GetDb();
|
||||
if (!(profile is HumanoidCharacterProfile humanoid))
|
||||
{
|
||||
// TODO: Handle other ICharacterProfile implementations properly
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
var entity = ConvertProfiles(humanoid, slot);
|
||||
|
||||
var prefs = await db.DbContext
|
||||
.Preference
|
||||
.Include(p => p.Profiles)
|
||||
.SingleAsync(p => p.UserId == userId.UserId);
|
||||
|
||||
var oldProfile = prefs
|
||||
.Profiles
|
||||
.SingleOrDefault(h => h.Slot == entity.Slot);
|
||||
|
||||
if (!(oldProfile is null))
|
||||
{
|
||||
prefs.Profiles.Remove(oldProfile);
|
||||
}
|
||||
|
||||
prefs.Profiles.Add(entity);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteCharacterSlotAsync(NetUserId userId, int slot)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
db.DbContext
|
||||
.Preference
|
||||
.Single(p => p.UserId == userId.UserId)
|
||||
.Profiles
|
||||
.RemoveAll(h => h.Slot == slot);
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
var profile = ConvertProfiles((HumanoidCharacterProfile) defaultProfile, 0);
|
||||
var prefs = new Preference
|
||||
{
|
||||
UserId = userId.UserId,
|
||||
SelectedCharacterSlot = 0
|
||||
};
|
||||
|
||||
prefs.Profiles.Add(profile);
|
||||
|
||||
db.DbContext.Preference.Add(prefs);
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
|
||||
return new PlayerPreferences(new []{defaultProfile}, 0);
|
||||
}
|
||||
|
||||
private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
|
||||
{
|
||||
var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority);
|
||||
var antags = profile.Antags.Select(a => a.AntagName);
|
||||
return new HumanoidCharacterProfile(
|
||||
profile.CharacterName,
|
||||
profile.Age,
|
||||
profile.Sex == "Male" ? Sex.Male : Sex.Female,
|
||||
new HumanoidCharacterAppearance
|
||||
(
|
||||
profile.HairName,
|
||||
Color.FromHex(profile.HairColor),
|
||||
profile.FacialHairName,
|
||||
Color.FromHex(profile.FacialHairColor),
|
||||
Color.FromHex(profile.EyeColor),
|
||||
Color.FromHex(profile.SkinColor)
|
||||
),
|
||||
jobs,
|
||||
(PreferenceUnavailableMode) profile.PreferenceUnavailable,
|
||||
antags.ToList()
|
||||
);
|
||||
}
|
||||
|
||||
private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot)
|
||||
{
|
||||
var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
|
||||
|
||||
var entity = new Profile
|
||||
{
|
||||
CharacterName = humanoid.Name,
|
||||
Age = humanoid.Age,
|
||||
Sex = humanoid.Sex.ToString(),
|
||||
HairName = appearance.HairStyleName,
|
||||
HairColor = appearance.HairColor.ToHex(),
|
||||
FacialHairName = appearance.FacialHairStyleName,
|
||||
FacialHairColor = appearance.FacialHairColor.ToHex(),
|
||||
EyeColor = appearance.EyeColor.ToHex(),
|
||||
SkinColor = appearance.SkinColor.ToHex(),
|
||||
Slot = slot,
|
||||
PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable
|
||||
};
|
||||
entity.Jobs.AddRange(
|
||||
humanoid.JobPriorities
|
||||
.Where(j => j.Value != JobPriority.Never)
|
||||
.Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value})
|
||||
);
|
||||
entity.Antags.AddRange(
|
||||
humanoid.AntagPreferences
|
||||
.Select(a => new Antag {AntagName = a})
|
||||
);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task<NetUserId?> GetAssignedUserIdAsync(string name)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
var assigned = await db.DbContext.AssignedUserId.SingleOrDefaultAsync(p => p.UserName == name);
|
||||
return assigned?.UserId is { } g ? new NetUserId(g) : default(NetUserId?);
|
||||
}
|
||||
|
||||
public async Task AssignUserIdAsync(string name, NetUserId netUserId)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
db.DbContext.AssignedUserId.Add(new AssignedUserId
|
||||
{
|
||||
UserId = netUserId.UserId,
|
||||
UserName = name
|
||||
});
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/*
|
||||
* BAN STUFF
|
||||
*/
|
||||
public abstract Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId);
|
||||
public abstract Task AddServerBanAsync(ServerBanDef serverBan);
|
||||
|
||||
/*
|
||||
* PLAYER RECORDS
|
||||
*/
|
||||
public abstract Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address);
|
||||
|
||||
/*
|
||||
* CONNECTION LOG
|
||||
*/
|
||||
public abstract Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address);
|
||||
|
||||
|
||||
protected abstract Task<DbGuard> GetDb();
|
||||
|
||||
protected abstract class DbGuard : IAsyncDisposable
|
||||
{
|
||||
public abstract ServerDbContext DbContext { get; }
|
||||
|
||||
public abstract ValueTask DisposeAsync();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
245
Content.Server/Database/ServerDbManager.cs
Normal file
245
Content.Server/Database/ServerDbManager.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared;
|
||||
using Content.Shared.Preferences;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using Robust.Shared.Interfaces.Configuration;
|
||||
using Robust.Shared.Interfaces.Log;
|
||||
using Robust.Shared.Interfaces.Resources;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
using LogLevel = Robust.Shared.Log.LogLevel;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Server.Database
|
||||
{
|
||||
public interface IServerDbManager
|
||||
{
|
||||
void Init();
|
||||
|
||||
// Preferences
|
||||
Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile);
|
||||
Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index);
|
||||
Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile profile, int slot);
|
||||
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId);
|
||||
|
||||
// Username assignment (for guest accounts, so they persist GUID)
|
||||
Task AssignUserIdAsync(string name, NetUserId userId);
|
||||
Task<NetUserId?> GetAssignedUserIdAsync(string name);
|
||||
|
||||
// Ban stuff
|
||||
Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId);
|
||||
Task AddServerBanAsync(ServerBanDef serverBan);
|
||||
|
||||
// Player records
|
||||
Task UpdatePlayerRecordAsync(NetUserId userId, string userName, IPAddress address);
|
||||
|
||||
// Connection log
|
||||
Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address);
|
||||
}
|
||||
|
||||
public sealed class ServerDbManager : IServerDbManager
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
[Dependency] private readonly ILogManager _logMgr = default!;
|
||||
|
||||
private ServerDbBase _db = default!;
|
||||
private LoggingProvider _msLogProvider = default!;
|
||||
private ILoggerFactory _msLoggerFactory = default!;
|
||||
|
||||
|
||||
public void Init()
|
||||
{
|
||||
_msLogProvider = new LoggingProvider(_logMgr);
|
||||
_msLoggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.AddProvider(_msLogProvider);
|
||||
});
|
||||
|
||||
var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower();
|
||||
switch (engine)
|
||||
{
|
||||
case "sqlite":
|
||||
var options = CreateSqliteOptions();
|
||||
_db = new ServerDbSqlite(options);
|
||||
break;
|
||||
case "postgres":
|
||||
options = CreatePostgresOptions();
|
||||
_db = new ServerDbPostgres(options);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException("Unknown database engine {engine}.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
|
||||
{
|
||||
return _db.InitPrefsAsync(userId, defaultProfile);
|
||||
}
|
||||
|
||||
public Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
|
||||
{
|
||||
return _db.SaveSelectedCharacterIndexAsync(userId, index);
|
||||
}
|
||||
|
||||
public Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile profile, int slot)
|
||||
{
|
||||
return _db.SaveCharacterSlotAsync(userId, profile, slot);
|
||||
}
|
||||
|
||||
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
|
||||
{
|
||||
return _db.GetPlayerPreferencesAsync(userId);
|
||||
}
|
||||
|
||||
public Task AssignUserIdAsync(string name, NetUserId userId)
|
||||
{
|
||||
return _db.AssignUserIdAsync(name, userId);
|
||||
}
|
||||
|
||||
public Task<NetUserId?> GetAssignedUserIdAsync(string name)
|
||||
{
|
||||
return _db.GetAssignedUserIdAsync(name);
|
||||
}
|
||||
|
||||
public Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId)
|
||||
{
|
||||
return _db.GetServerBanAsync(address, userId);
|
||||
}
|
||||
|
||||
public Task AddServerBanAsync(ServerBanDef serverBan)
|
||||
{
|
||||
return _db.AddServerBanAsync(serverBan);
|
||||
}
|
||||
|
||||
public Task UpdatePlayerRecordAsync(NetUserId userId, string userName, IPAddress address)
|
||||
{
|
||||
return _db.UpdatePlayerRecord(userId, userName, address);
|
||||
}
|
||||
|
||||
public Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address)
|
||||
{
|
||||
return _db.AddConnectionLogAsync(userId, userName, address);
|
||||
}
|
||||
|
||||
private DbContextOptions<ServerDbContext> CreatePostgresOptions()
|
||||
{
|
||||
var host = _cfg.GetCVar(CCVars.DatabasePgHost);
|
||||
var port = _cfg.GetCVar(CCVars.DatabasePgPort);
|
||||
var db = _cfg.GetCVar(CCVars.DatabasePgDatabase);
|
||||
var user = _cfg.GetCVar(CCVars.DatabasePgUsername);
|
||||
var pass = _cfg.GetCVar(CCVars.DatabasePgPassword);
|
||||
|
||||
var builder = new DbContextOptionsBuilder<ServerDbContext>();
|
||||
var connectionString = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = host,
|
||||
Port = port,
|
||||
Database = db,
|
||||
Username = user,
|
||||
Password = pass
|
||||
}.ConnectionString;
|
||||
builder.UseNpgsql(connectionString);
|
||||
SetupLogging(builder);
|
||||
return builder.Options;
|
||||
}
|
||||
|
||||
private DbContextOptions<ServerDbContext> CreateSqliteOptions()
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<ServerDbContext>();
|
||||
|
||||
var configPreferencesDbPath = _cfg.GetCVar(CCVars.DatabaseSqliteDbPath);
|
||||
var inMemory = _res.UserData.RootDir == null;
|
||||
|
||||
SqliteConnection connection;
|
||||
if (!inMemory)
|
||||
{
|
||||
var finalPreferencesDbPath = Path.Combine(_res.UserData.RootDir!, configPreferencesDbPath);
|
||||
connection = new SqliteConnection($"Data Source={finalPreferencesDbPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
connection = new SqliteConnection("Data Source=:memory:");
|
||||
// When using an in-memory DB we have to open it manually
|
||||
// so EFCore doesn't open, close and wipe it.
|
||||
connection.Open();
|
||||
}
|
||||
|
||||
builder.UseSqlite(connection);
|
||||
SetupLogging(builder);
|
||||
return builder.Options;
|
||||
}
|
||||
|
||||
private void SetupLogging(DbContextOptionsBuilder<ServerDbContext> builder)
|
||||
{
|
||||
builder.UseLoggerFactory(_msLoggerFactory);
|
||||
}
|
||||
|
||||
private sealed class LoggingProvider : ILoggerProvider
|
||||
{
|
||||
private readonly ILogManager _logManager;
|
||||
|
||||
public LoggingProvider(ILogManager logManager)
|
||||
{
|
||||
_logManager = logManager;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new MSLogger(_logManager.GetSawmill("db.ef"));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MSLogger : ILogger
|
||||
{
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public MSLogger(ISawmill sawmill)
|
||||
{
|
||||
_sawmill = sawmill;
|
||||
}
|
||||
|
||||
public void Log<TState>(MSLogLevel logLevel, EventId eventId, TState state, Exception exception,
|
||||
Func<TState, Exception, string> formatter)
|
||||
{
|
||||
var lvl = logLevel switch
|
||||
{
|
||||
MSLogLevel.Trace => LogLevel.Debug,
|
||||
MSLogLevel.Debug => LogLevel.Debug,
|
||||
// EFCore feels the need to log individual DB commands as "Information" so I'm slapping debug on it.
|
||||
MSLogLevel.Information => LogLevel.Debug,
|
||||
MSLogLevel.Warning => LogLevel.Warning,
|
||||
MSLogLevel.Error => LogLevel.Error,
|
||||
MSLogLevel.Critical => LogLevel.Fatal,
|
||||
MSLogLevel.None => LogLevel.Debug,
|
||||
_ => LogLevel.Debug
|
||||
};
|
||||
|
||||
_sawmill.Log(lvl, formatter(state, exception));
|
||||
}
|
||||
|
||||
public bool IsEnabled(MSLogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
// TODO: this
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
184
Content.Server/Database/ServerDbPostgres.cs
Normal file
184
Content.Server/Database/ServerDbPostgres.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Server.Database
|
||||
{
|
||||
public sealed class ServerDbPostgres : ServerDbBase
|
||||
{
|
||||
private readonly DbContextOptions<ServerDbContext> _options;
|
||||
private readonly Task _dbReadyTask;
|
||||
|
||||
public ServerDbPostgres(DbContextOptions<ServerDbContext> options)
|
||||
{
|
||||
_options = options;
|
||||
|
||||
_dbReadyTask = Task.Run(async () =>
|
||||
{
|
||||
await using var ctx = new PostgresServerDbContext(_options);
|
||||
try
|
||||
{
|
||||
await ctx.Database.MigrateAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ctx.DisposeAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId)
|
||||
{
|
||||
if (address == null && userId == null)
|
||||
{
|
||||
throw new ArgumentException("Address and userId cannot both be null");
|
||||
}
|
||||
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
var query = db.PgDbContext.Ban
|
||||
.Include(p => p.Unban)
|
||||
.Where(p => p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now));
|
||||
|
||||
if (userId is { } uid)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
// Only have a user ID.
|
||||
query = query.Where(p => p.UserId == uid.UserId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Have both user ID and IP address.
|
||||
query = query.Where(p =>
|
||||
(p.Address != null && EF.Functions.ContainsOrEqual(p.Address.Value, address))
|
||||
|| p.UserId == uid.UserId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only have a connecting address.
|
||||
query = query.Where(
|
||||
p => p.Address != null && EF.Functions.ContainsOrEqual(p.Address.Value, address));
|
||||
}
|
||||
|
||||
var ban = await query.FirstOrDefaultAsync();
|
||||
|
||||
return ConvertBan(ban);
|
||||
}
|
||||
|
||||
private static ServerBanDef? ConvertBan(PostgresServerBan? ban)
|
||||
{
|
||||
if (ban == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
NetUserId? uid = null;
|
||||
if (ban.UserId is {} guid)
|
||||
{
|
||||
uid = new NetUserId(guid);
|
||||
}
|
||||
|
||||
NetUserId? aUid = null;
|
||||
if (ban.BanningAdmin is {} aGuid)
|
||||
{
|
||||
aUid = new NetUserId(aGuid);
|
||||
}
|
||||
|
||||
return new ServerBanDef(
|
||||
uid,
|
||||
ban.Address,
|
||||
ban.BanTime,
|
||||
ban.ExpirationTime,
|
||||
ban.Reason,
|
||||
aUid);
|
||||
}
|
||||
|
||||
public override async Task AddServerBanAsync(ServerBanDef serverBan)
|
||||
{
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
db.PgDbContext.Ban.Add(new PostgresServerBan
|
||||
{
|
||||
Address = serverBan.Address,
|
||||
Reason = serverBan.Reason,
|
||||
BanningAdmin = serverBan.BanningAdmin?.UserId,
|
||||
BanTime = serverBan.BanTime.UtcDateTime,
|
||||
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
|
||||
UserId = serverBan.UserId?.UserId
|
||||
});
|
||||
|
||||
await db.PgDbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public override async Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address)
|
||||
{
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
var record = await db.PgDbContext.Player.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
|
||||
if (record == null)
|
||||
{
|
||||
db.PgDbContext.Player.Add(record = new PostgresPlayer
|
||||
{
|
||||
FirstSeenTime = DateTime.UtcNow,
|
||||
UserId = userId.UserId,
|
||||
});
|
||||
}
|
||||
|
||||
record.LastSeenTime = DateTime.UtcNow;
|
||||
record.LastSeenAddress = address;
|
||||
record.LastSeenUserName = userName;
|
||||
|
||||
await db.PgDbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public override async Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address)
|
||||
{
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
db.PgDbContext.ConnectionLog.Add(new PostgresConnectionLog
|
||||
{
|
||||
Address = address,
|
||||
Time = DateTime.UtcNow,
|
||||
UserId = userId.UserId,
|
||||
UserName = userName
|
||||
});
|
||||
|
||||
await db.PgDbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task<DbGuardImpl> GetDbImpl()
|
||||
{
|
||||
await _dbReadyTask;
|
||||
|
||||
return new DbGuardImpl(new PostgresServerDbContext(_options));
|
||||
}
|
||||
|
||||
protected override async Task<DbGuard> GetDb()
|
||||
{
|
||||
return await GetDbImpl();
|
||||
}
|
||||
|
||||
private sealed class DbGuardImpl : DbGuard
|
||||
{
|
||||
public DbGuardImpl(PostgresServerDbContext dbC)
|
||||
{
|
||||
PgDbContext = dbC;
|
||||
}
|
||||
|
||||
public PostgresServerDbContext PgDbContext { get; }
|
||||
public override ServerDbContext DbContext => PgDbContext;
|
||||
|
||||
public override ValueTask DisposeAsync()
|
||||
{
|
||||
return DbContext.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
192
Content.Server/Database/ServerDbSqlite.cs
Normal file
192
Content.Server/Database/ServerDbSqlite.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Preferences;
|
||||
using Content.Server.Utility;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Server.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods to retrieve and update character preferences.
|
||||
/// Don't use this directly, go through <see cref="ServerPreferencesManager" /> instead.
|
||||
/// </summary>
|
||||
public sealed class ServerDbSqlite : ServerDbBase
|
||||
{
|
||||
// For SQLite we use a single DB context via SQLite.
|
||||
// This doesn't allow concurrent access so that's what the semaphore is for.
|
||||
// That said, this is bloody SQLite, I don't even think EFCore bothers to truly async it.
|
||||
private readonly SemaphoreSlim _prefsSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
private readonly Task _dbReadyTask;
|
||||
private readonly SqliteServerDbContext _prefsCtx;
|
||||
|
||||
public ServerDbSqlite(DbContextOptions<ServerDbContext> options)
|
||||
{
|
||||
_prefsCtx = new SqliteServerDbContext(options);
|
||||
|
||||
_dbReadyTask = Task.Run(() => _prefsCtx.Database.Migrate());
|
||||
}
|
||||
|
||||
public override async Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId)
|
||||
{
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
// SQLite can't do the net masking stuff we need to match IP address ranges.
|
||||
// So just pull down the whole list into memory.
|
||||
var bans = await db.SqliteDbContext.Ban
|
||||
.Include(p => p.Unban)
|
||||
.Where(p => p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var ban in bans)
|
||||
{
|
||||
if (address != null && ban.Address != null && address.IsInSubnet(ban.Address))
|
||||
{
|
||||
return ConvertBan(ban);
|
||||
}
|
||||
|
||||
if (userId is { } id && ban.UserId == id.UserId)
|
||||
{
|
||||
return ConvertBan(ban);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override async Task AddServerBanAsync(ServerBanDef serverBan)
|
||||
{
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
string? addrStr = null;
|
||||
if (serverBan.Address is { } addr)
|
||||
{
|
||||
addrStr = $"{addr.address}/{addr.cidrMask}";
|
||||
}
|
||||
|
||||
db.SqliteDbContext.Ban.Add(new SqliteServerBan
|
||||
{
|
||||
Address = addrStr,
|
||||
Reason = serverBan.Reason,
|
||||
BanningAdmin = serverBan.BanningAdmin?.UserId,
|
||||
BanTime = serverBan.BanTime.UtcDateTime,
|
||||
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
|
||||
UserId = serverBan.UserId?.UserId
|
||||
});
|
||||
|
||||
await db.SqliteDbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public override async Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address)
|
||||
{
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
var record = await db.SqliteDbContext.Player.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
|
||||
if (record == null)
|
||||
{
|
||||
db.SqliteDbContext.Player.Add(record = new SqlitePlayer
|
||||
{
|
||||
FirstSeenTime = DateTime.UtcNow,
|
||||
UserId = userId.UserId,
|
||||
});
|
||||
}
|
||||
|
||||
record.LastSeenTime = DateTime.UtcNow;
|
||||
record.LastSeenAddress = address.ToString();
|
||||
record.LastSeenUserName = userName;
|
||||
|
||||
await db.SqliteDbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static ServerBanDef? ConvertBan(SqliteServerBan? ban)
|
||||
{
|
||||
if (ban == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
NetUserId? uid = null;
|
||||
if (ban.UserId is {} guid)
|
||||
{
|
||||
uid = new NetUserId(guid);
|
||||
}
|
||||
|
||||
NetUserId? aUid = null;
|
||||
if (ban.BanningAdmin is {} aGuid)
|
||||
{
|
||||
aUid = new NetUserId(aGuid);
|
||||
}
|
||||
|
||||
(IPAddress, int)? addrTuple = null;
|
||||
if (ban.Address != null)
|
||||
{
|
||||
var idx = ban.Address.IndexOf('/', StringComparison.Ordinal);
|
||||
addrTuple = (IPAddress.Parse(ban.Address.AsSpan(0, idx)),
|
||||
int.Parse(ban.Address.AsSpan(idx + 1), provider: CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return new ServerBanDef(
|
||||
uid,
|
||||
addrTuple,
|
||||
ban.BanTime,
|
||||
ban.ExpirationTime,
|
||||
ban.Reason,
|
||||
aUid);
|
||||
}
|
||||
|
||||
public override async Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address)
|
||||
{
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
db.SqliteDbContext.ConnectionLog.Add(new SqliteConnectionLog
|
||||
{
|
||||
Address = address.ToString(),
|
||||
Time = DateTime.UtcNow,
|
||||
UserId = userId.UserId,
|
||||
UserName = userName
|
||||
});
|
||||
|
||||
await db.SqliteDbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
private async Task<DbGuardImpl> GetDbImpl()
|
||||
{
|
||||
await _dbReadyTask;
|
||||
await _prefsSemaphore.WaitAsync();
|
||||
|
||||
return new DbGuardImpl(this);
|
||||
}
|
||||
|
||||
protected override async Task<DbGuard> GetDb()
|
||||
{
|
||||
return await GetDbImpl();
|
||||
}
|
||||
|
||||
private sealed class DbGuardImpl : DbGuard
|
||||
{
|
||||
private readonly ServerDbSqlite _db;
|
||||
|
||||
public DbGuardImpl(ServerDbSqlite db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public override ServerDbContext DbContext => _db._prefsCtx;
|
||||
public SqliteServerDbContext SqliteDbContext => _db._prefsCtx;
|
||||
|
||||
public override ValueTask DisposeAsync()
|
||||
{
|
||||
_db._prefsSemaphore.Release();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user