This commit is contained in:
2025-08-22 11:55:35 +01:00
commit b21d2a685e
312 changed files with 31174 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Reflection;
using Microsoft.Extensions.Logging;
namespace MareSynchronosShared.Utils;
public class AllowedControllersFeatureProvider : ControllerFeatureProvider
{
private readonly ILogger _logger;
private readonly Type[] _allowedTypes;
public AllowedControllersFeatureProvider(params Type[] allowedTypes)
{
_allowedTypes = allowedTypes;
}
protected override bool IsController(TypeInfo typeInfo)
{
return base.IsController(typeInfo) && _allowedTypes.Contains(typeInfo.AsType());
}
}

View File

@@ -0,0 +1,4 @@
using MareSynchronos.API.Data.Enum;
namespace MareSynchronosShared.Utils;
public record ClientMessage(MessageSeverity Severity, string Message, string UID);

View File

@@ -0,0 +1,29 @@
using System.Text;
namespace MareSynchronosShared.Utils.Configuration;
public class AuthServiceConfiguration : MareConfigurationBase
{
public string GeoIPDbCityFile { get; set; } = string.Empty;
public bool UseGeoIP { get; set; } = false;
public int FailedAuthForTempBan { get; set; } = 5;
public int TempBanDurationInMinutes { get; set; } = 5;
public List<string> WhitelistedIps { get; set; } = new();
public int RegisterIpLimit { get; set; } = 3;
public int RegisterIpDurationInMinutes { get; set; } = 10;
public string WellKnown { get; set; } = string.Empty;
public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine(base.ToString());
sb.AppendLine($"{nameof(GeoIPDbCityFile)} => {GeoIPDbCityFile}");
sb.AppendLine($"{nameof(UseGeoIP)} => {UseGeoIP}");
sb.AppendLine($"{nameof(RegisterIpLimit)} => {RegisterIpLimit}");
sb.AppendLine($"{nameof(RegisterIpDurationInMinutes)} => {RegisterIpDurationInMinutes}");
sb.AppendLine($"{nameof(WellKnown)} => {WellKnown}");
return sb.ToString();
}
}

View File

@@ -0,0 +1,13 @@
namespace MareSynchronosShared.Utils.Configuration;
public class CdnShardConfiguration
{
public List<string> Continents { get; set; }
public string FileMatch { get; set; }
public Uri CdnFullUrl { get; set; }
public override string ToString()
{
return CdnFullUrl.ToString() + "[" + string.Join(',', Continents) + "] == " + FileMatch;
}
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronosShared.Utils.Configuration;
public interface IMareConfiguration
{
T GetValueOrDefault<T>(string key, T defaultValue);
T GetValue<T>(string key);
string SerializeValue(string key, string defaultValue);
}

View File

@@ -0,0 +1,51 @@
using System.Reflection;
using System.Text;
using System.Text.Json;
namespace MareSynchronosShared.Utils.Configuration;
public class MareConfigurationBase : IMareConfiguration
{
public int DbContextPoolSize { get; set; } = 100;
public string Jwt { get; set; } = string.Empty;
public Uri MainServerAddress { get; set; }
public int RedisPool { get; set; } = 50;
public int MetricsPort { get; set; }
public string RedisConnectionString { get; set; } = string.Empty;
public string ShardName { get; set; } = string.Empty;
public T GetValue<T>(string key)
{
var prop = GetType().GetProperty(key);
if (prop == null) throw new KeyNotFoundException(key);
if (prop.PropertyType != typeof(T)) throw new ArgumentException($"Requested {key} with T:{typeof(T)}, where {key} is {prop.PropertyType}");
return (T)prop.GetValue(this);
}
public T GetValueOrDefault<T>(string key, T defaultValue)
{
var prop = GetType().GetProperty(key);
if (prop.PropertyType != typeof(T)) throw new ArgumentException($"Requested {key} with T:{typeof(T)}, where {key} is {prop.PropertyType}");
if (prop == null) return defaultValue;
return (T)prop.GetValue(this);
}
public string SerializeValue(string key, string defaultValue)
{
var prop = GetType().GetProperty(key);
if (prop == null) return defaultValue;
if (prop.GetCustomAttribute<RemoteConfigurationAttribute>() == null) return defaultValue;
return JsonSerializer.Serialize(prop.GetValue(this), prop.PropertyType);
}
public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine(base.ToString());
sb.AppendLine($"{nameof(MainServerAddress)} => {MainServerAddress}");
sb.AppendLine($"{nameof(RedisConnectionString)} => {RedisConnectionString}");
sb.AppendLine($"{nameof(ShardName)} => {ShardName}");
sb.AppendLine($"{nameof(DbContextPoolSize)} => {DbContextPoolSize}");
return sb.ToString();
}
}

View File

@@ -0,0 +1,46 @@
using System.Text;
namespace MareSynchronosShared.Utils.Configuration;
public class ServerConfiguration : MareConfigurationBase
{
[RemoteConfiguration]
public Uri CdnFullUrl { get; set; } = null;
[RemoteConfiguration]
public Version ExpectedClientVersion { get; set; } = new Version(0, 0, 0);
[RemoteConfiguration]
public int MaxExistingGroupsByUser { get; set; } = 3;
[RemoteConfiguration]
public int MaxGroupUserCount { get; set; } = 100;
[RemoteConfiguration]
public int MaxJoinedGroupsByUser { get; set; } = 6;
[RemoteConfiguration]
public bool PurgeUnusedAccounts { get; set; } = false;
[RemoteConfiguration]
public int PurgeUnusedAccountsPeriodInDays { get; set; } = 14;
[RemoteConfiguration]
public int MaxCharaDataByUser { get; set; } = 10;
public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine(base.ToString());
sb.AppendLine($"{nameof(CdnFullUrl)} => {CdnFullUrl}");
sb.AppendLine($"{nameof(RedisConnectionString)} => {RedisConnectionString}");
sb.AppendLine($"{nameof(ExpectedClientVersion)} => {ExpectedClientVersion}");
sb.AppendLine($"{nameof(MaxExistingGroupsByUser)} => {MaxExistingGroupsByUser}");
sb.AppendLine($"{nameof(MaxJoinedGroupsByUser)} => {MaxJoinedGroupsByUser}");
sb.AppendLine($"{nameof(MaxGroupUserCount)} => {MaxGroupUserCount}");
sb.AppendLine($"{nameof(PurgeUnusedAccounts)} => {PurgeUnusedAccounts}");
sb.AppendLine($"{nameof(PurgeUnusedAccountsPeriodInDays)} => {PurgeUnusedAccountsPeriodInDays}");
sb.AppendLine($"{nameof(MaxCharaDataByUser)} => {MaxCharaDataByUser}");
return sb.ToString();
}
}

View File

@@ -0,0 +1,21 @@
using System.Text;
namespace MareSynchronosShared.Utils.Configuration;
public class ServicesConfiguration : MareConfigurationBase
{
public string DiscordBotToken { get; set; } = string.Empty;
public ulong? DiscordChannelForMessages { get; set; } = null;
public ulong? DiscordChannelForReports { get; set; } = null;
public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine(base.ToString());
sb.AppendLine($"{nameof(DiscordBotToken)} => {DiscordBotToken}");
sb.AppendLine($"{nameof(MainServerAddress)} => {MainServerAddress}");
sb.AppendLine($"{nameof(DiscordChannelForMessages)} => {DiscordChannelForMessages}");
sb.AppendLine($"{nameof(DiscordChannelForReports)} => {DiscordChannelForReports}");
return sb.ToString();
}
}

View File

@@ -0,0 +1,67 @@
using MareSynchronosShared.Utils;
using System.Text;
namespace MareSynchronosShared.Utils.Configuration;
public class StaticFilesServerConfiguration : MareConfigurationBase
{
public bool IsDistributionNode { get; set; } = false;
public bool NotifyMainServerDirectly { get; set; } = false;
public Uri MainFileServerAddress { get; set; } = null;
public Uri DistributionFileServerAddress { get; set; } = null;
public bool DistributionFileServerForceHTTP2 { get; set; } = false;
public int ForcedDeletionOfFilesAfterHours { get; set; } = -1;
public double CacheSizeHardLimitInGiB { get; set; } = -1;
public int MinimumFileRetentionPeriodInDays { get; set; } = 7;
public int UnusedFileRetentionPeriodInDays { get; set; } = 14;
public string CacheDirectory { get; set; }
public int DownloadQueueSize { get; set; } = 50;
public int DownloadTimeoutSeconds { get; set; } = 5;
public int DownloadQueueReleaseSeconds { get; set; } = 15;
public int DownloadQueueClearLimit { get; set; } = 15000;
public int CleanupCheckInMinutes { get; set; } = 15;
public bool UseColdStorage { get; set; } = false;
public string ColdStorageDirectory { get; set; } = null;
public double ColdStorageSizeHardLimitInGiB { get; set; } = -1;
public int ColdStorageMinimumFileRetentionPeriodInDays { get; set; } = 30;
public int ColdStorageUnusedFileRetentionPeriodInDays { get; set; } = 30;
public double CacheSmallSizeThresholdKiB { get; set; } = 64;
public double CacheLargeSizeThresholdKiB { get; set; } = 1024;
[RemoteConfiguration]
public Uri CdnFullUrl { get; set; } = null;
[RemoteConfiguration]
public List<CdnShardConfiguration> CdnShardConfiguration { get; set; } = new();
public bool UseXAccelRedirect { get; set; } = false;
public string XAccelRedirectPrefix { get; set; } = "/_internal/mare-files/";
public bool UseSSI { get; set; } = false;
public string SSIContentType { get; set; } = "application/x-block-file-list";
public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine(base.ToString());
sb.AppendLine($"{nameof(IsDistributionNode)} => {IsDistributionNode}");
sb.AppendLine($"{nameof(NotifyMainServerDirectly)} => {NotifyMainServerDirectly}");
sb.AppendLine($"{nameof(MainFileServerAddress)} => {MainFileServerAddress}");
sb.AppendLine($"{nameof(DistributionFileServerAddress)} => {DistributionFileServerAddress}");
sb.AppendLine($"{nameof(DistributionFileServerForceHTTP2)} => {DistributionFileServerForceHTTP2}");
sb.AppendLine($"{nameof(ForcedDeletionOfFilesAfterHours)} => {ForcedDeletionOfFilesAfterHours}");
sb.AppendLine($"{nameof(CacheSizeHardLimitInGiB)} => {CacheSizeHardLimitInGiB}");
sb.AppendLine($"{nameof(UseColdStorage)} => {UseColdStorage}");
sb.AppendLine($"{nameof(ColdStorageDirectory)} => {ColdStorageDirectory}");
sb.AppendLine($"{nameof(ColdStorageSizeHardLimitInGiB)} => {ColdStorageSizeHardLimitInGiB}");
sb.AppendLine($"{nameof(ColdStorageMinimumFileRetentionPeriodInDays)} => {ColdStorageMinimumFileRetentionPeriodInDays}");
sb.AppendLine($"{nameof(ColdStorageUnusedFileRetentionPeriodInDays)} => {ColdStorageUnusedFileRetentionPeriodInDays}");
sb.AppendLine($"{nameof(MinimumFileRetentionPeriodInDays)} => {MinimumFileRetentionPeriodInDays}");
sb.AppendLine($"{nameof(UnusedFileRetentionPeriodInDays)} => {UnusedFileRetentionPeriodInDays}");
sb.AppendLine($"{nameof(CacheSmallSizeThresholdKiB)} => {CacheSmallSizeThresholdKiB}");
sb.AppendLine($"{nameof(CacheLargeSizeThresholdKiB)} => {CacheLargeSizeThresholdKiB}");
sb.AppendLine($"{nameof(CacheDirectory)} => {CacheDirectory}");
sb.AppendLine($"{nameof(DownloadQueueSize)} => {DownloadQueueSize}");
sb.AppendLine($"{nameof(DownloadQueueReleaseSeconds)} => {DownloadQueueReleaseSeconds}");
sb.AppendLine($"{nameof(CdnShardConfiguration)} => {string.Join(", ", CdnShardConfiguration)}");
sb.AppendLine($"{nameof(UseXAccelRedirect)} => {UseXAccelRedirect}");
return sb.ToString();
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.SignalR;
namespace MareSynchronosShared.Utils;
public class IdBasedUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext context)
{
return context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value;
}
}

View File

@@ -0,0 +1,10 @@
namespace MareSynchronosShared.Utils;
public static class MareClaimTypes
{
public const string Uid = "uid";
public const string Alias = "alias";
public const string CharaIdent = "character_identification";
public const string Internal = "internal";
public const string Continent = "continent";
}

View File

@@ -0,0 +1,4 @@
namespace MareSynchronosShared.Utils;
[AttributeUsage(AttributeTargets.Property)]
public class RemoteConfigurationAttribute : Attribute { }

View File

@@ -0,0 +1,61 @@
using MareSynchronosShared.Utils.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace MareSynchronosShared.Utils;
public class ServerTokenGenerator
{
private readonly IOptionsMonitor<MareConfigurationBase> _configuration;
private readonly ILogger<ServerTokenGenerator> _logger;
private Dictionary<string, string> _tokenDictionary { get; set; } = new(StringComparer.Ordinal);
public string Token
{
get
{
var currentJwt = _configuration.CurrentValue.Jwt;
if (_tokenDictionary.TryGetValue(currentJwt, out var token))
{
return token;
}
return GenerateToken();
}
}
public ServerTokenGenerator(IOptionsMonitor<MareConfigurationBase> configuration, ILogger<ServerTokenGenerator> logger)
{
_configuration = configuration;
_logger = logger;
}
private string GenerateToken()
{
var signingKey = _configuration.CurrentValue.Jwt;
var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(signingKey));
var token = new SecurityTokenDescriptor()
{
Subject = new ClaimsIdentity(new List<Claim>()
{
new Claim(MareClaimTypes.Uid, _configuration.CurrentValue.ShardName),
new Claim(MareClaimTypes.Internal, "true"),
}),
SigningCredentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature),
};
var handler = new JwtSecurityTokenHandler();
var rawData = handler.CreateJwtSecurityToken(token).RawData;
_tokenDictionary[signingKey] = rawData;
_logger.LogInformation("Generated Token: {data}", rawData);
return rawData;
}
}

View File

@@ -0,0 +1,124 @@
using MareSynchronosShared.Data;
using MareSynchronosShared.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MareSynchronosShared.Utils;
public static class SharedDbFunctions
{
public static async Task<(bool, string)> MigrateOrDeleteGroup(MareDbContext context, Group group, List<GroupPair> groupPairs, int maxGroupsByUser)
{
bool groupHasMigrated = false;
string newOwner = string.Empty;
foreach (var potentialNewOwner in groupPairs.OrderByDescending(p => p.IsModerator).ThenByDescending(p => p.IsPinned).ToList())
{
groupHasMigrated = await TryMigrateGroup(context, group, potentialNewOwner.GroupUserUID, maxGroupsByUser).ConfigureAwait(false);
if (groupHasMigrated)
{
newOwner = potentialNewOwner.GroupUserUID;
potentialNewOwner.IsPinned = true;
potentialNewOwner.IsModerator = false;
await context.SaveChangesAsync().ConfigureAwait(false);
break;
}
}
if (!groupHasMigrated)
{
context.GroupPairs.RemoveRange(groupPairs);
context.Groups.Remove(group);
await context.SaveChangesAsync().ConfigureAwait(false);
}
return (groupHasMigrated, newOwner);
}
public static async Task PurgeUser(ILogger _logger, User user, MareDbContext dbContext, int maxGroupsByUser)
{
_logger.LogInformation("Purging user: {uid}", user.UID);
var secondaryUsers = await dbContext.Auth.Include(u => u.User)
.Where(u => u.PrimaryUserUID == user.UID).Select(c => c.User).ToListAsync().ConfigureAwait(false);
foreach (var secondaryUser in secondaryUsers)
{
await PurgeUser(_logger, secondaryUser, dbContext, maxGroupsByUser).ConfigureAwait(false);
}
var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID);
var userProfileData = await dbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false);
if (lodestone != null)
{
dbContext.Remove(lodestone);
}
if (userProfileData != null)
{
dbContext.Remove(userProfileData);
}
var auth = dbContext.Auth.Single(a => a.UserUID == user.UID);
var ownPairData = dbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToList();
dbContext.ClientPairs.RemoveRange(ownPairData);
var otherPairData = dbContext.ClientPairs.Include(u => u.User)
.Where(u => u.OtherUser.UID == user.UID).ToList();
dbContext.ClientPairs.RemoveRange(otherPairData);
var userJoinedGroups = await dbContext.GroupPairs.Include(g => g.Group).Where(u => u.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false);
foreach (var userGroupPair in userJoinedGroups)
{
bool ownerHasLeft = string.Equals(userGroupPair.Group.OwnerUID, user.UID, StringComparison.Ordinal);
if (ownerHasLeft)
{
var groupPairs = await dbContext.GroupPairs.Where(g => g.GroupGID == userGroupPair.GroupGID && g.GroupUserUID != user.UID).ToListAsync().ConfigureAwait(false);
if (!groupPairs.Any())
{
_logger.LogInformation("Group {gid} has no new owner, deleting", userGroupPair.GroupGID);
dbContext.Groups.Remove(userGroupPair.Group);
}
else
{
_ = await MigrateOrDeleteGroup(dbContext, userGroupPair.Group, groupPairs, maxGroupsByUser).ConfigureAwait(false);
}
}
dbContext.GroupPairs.Remove(userGroupPair);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
var bannedinGroups = await dbContext.GroupBans.Where(u => u.BannedUserUID == user.UID).ToListAsync().ConfigureAwait(false);
dbContext.GroupBans.RemoveRange(bannedinGroups);
_logger.LogInformation("User purged: {uid}", user.UID);
dbContext.Auth.Remove(auth);
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
private static async Task<bool> TryMigrateGroup(MareDbContext context, Group group, string potentialNewOwnerUid, int maxGroupsByUser)
{
var newOwnerOwnedGroups = await context.Groups.CountAsync(g => g.OwnerUID == potentialNewOwnerUid).ConfigureAwait(false);
if (newOwnerOwnedGroups >= maxGroupsByUser)
{
return false;
}
group.OwnerUID = potentialNewOwnerUid;
group.Alias = null;
await context.SaveChangesAsync().ConfigureAwait(false);
return true;
}
}

View File

@@ -0,0 +1,49 @@
using System.Security.Cryptography;
using System.Text;
namespace MareSynchronosShared.Utils;
public static class StringUtils
{
public static string GenerateRandomString(int length, string? allowableChars = null)
{
if (string.IsNullOrEmpty(allowableChars))
allowableChars = @"ABCDEFGHJKLMNPQRSTUVWXYZ0123456789";
// Generate random data
var rnd = RandomNumberGenerator.GetBytes(length);
// Generate the output string
var allowable = allowableChars.ToCharArray();
var l = allowable.Length;
var chars = new char[length];
for (var i = 0; i < length; i++)
chars[i] = allowable[rnd[i] % l];
return new string(chars);
}
public static string Sha256String(string input)
{
using var sha256 = SHA256.Create();
return BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(input))).Replace("-", "", StringComparison.OrdinalIgnoreCase);
}
}
public static class ListUtils
{
private static Random rng = new();
public static void Shuffle<T>(this IList<T> list)
{
int n = list.Count;
while (n > 1)
{
n--;
int k = rng.Next(n + 1);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
}