From 989046da130cb6177376a7bb13dc1bee94fdc818 Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Fri, 12 Jan 2024 13:10:14 +0100 Subject: [PATCH] add geoip service for file shard matching fix bug more logging fix logging wtf is going on even handle lists in config log output do not set "*" as default continent do not rely on "*" being present in configuration when picking file shard --- .../Controllers/JwtController.cs | 7 +- .../MareSynchronosServer.csproj | 1 + .../Services/GeoIPService.cs | 123 ++++++++++++++++++ .../MareSynchronosServer/Startup.cs | 2 + .../MareConfigurationServiceServer.cs | 18 ++- .../Utils/CdnShardConfiguration.cs | 3 +- .../Utils/MareClaimTypes.cs | 1 + .../Utils/MareConfigurationAuthBase.cs | 3 + .../Utils/ServerConfiguration.cs | 2 + .../Utils/StaticFilesServerConfiguration.cs | 1 + .../Controllers/ControllerBase.cs | 1 + .../Controllers/ServerFilesController.cs | 31 ++++- 12 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 MareSynchronosServer/MareSynchronosServer/Services/GeoIPService.cs diff --git a/MareSynchronosServer/MareSynchronosServer/Controllers/JwtController.cs b/MareSynchronosServer/MareSynchronosServer/Controllers/JwtController.cs index 242d547..c0412ab 100644 --- a/MareSynchronosServer/MareSynchronosServer/Controllers/JwtController.cs +++ b/MareSynchronosServer/MareSynchronosServer/Controllers/JwtController.cs @@ -1,5 +1,6 @@ using MareSynchronos.API.Routes; using MareSynchronosServer.Authentication; +using MareSynchronosServer.Services; using MareSynchronosShared; using MareSynchronosShared.Data; using MareSynchronosShared.Models; @@ -23,6 +24,7 @@ public class JwtController : Controller private readonly IHttpContextAccessor _accessor; private readonly IRedisDatabase _redis; private readonly MareDbContext _mareDbContext; + private readonly GeoIPService _geoIPProvider; private readonly SecretKeyAuthenticatorService _secretKeyAuthenticatorService; private readonly AccountRegistrationService _accountRegistrationService; private readonly IConfigurationService _configuration; @@ -31,10 +33,11 @@ public class JwtController : Controller SecretKeyAuthenticatorService secretKeyAuthenticatorService, AccountRegistrationService accountRegistrationService, IConfigurationService configuration, - IRedisDatabase redisDb) + IRedisDatabase redisDb, GeoIPService geoIPProvider) { _accessor = accessor; _redis = redisDb; + _geoIPProvider = geoIPProvider; _mareDbContext = mareDbContext; _secretKeyAuthenticatorService = secretKeyAuthenticatorService; _accountRegistrationService = accountRegistrationService; @@ -112,6 +115,7 @@ public class JwtController : Controller { new Claim(MareClaimTypes.Uid, authResult.Uid), new Claim(MareClaimTypes.CharaIdent, charaIdent), + new Claim(MareClaimTypes.Continent, await _geoIPProvider.GetCountryFromIP(_accessor)), }); return Content(token.RawData); @@ -140,3 +144,4 @@ public class JwtController : Controller return handler.CreateJwtSecurityToken(token); } } + diff --git a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj index b662f74..160d820 100644 --- a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj +++ b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj @@ -29,6 +29,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/MareSynchronosServer/MareSynchronosServer/Services/GeoIPService.cs b/MareSynchronosServer/MareSynchronosServer/Services/GeoIPService.cs new file mode 100644 index 0000000..eaa34b3 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Services/GeoIPService.cs @@ -0,0 +1,123 @@ +using MareSynchronosShared; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using MaxMind.GeoIP2; + +namespace MareSynchronosServer.Services; + +public class GeoIPService : IHostedService +{ + private readonly ILogger _logger; + private readonly IConfigurationService _mareConfiguration; + private bool _useGeoIP = false; + private string _countryFile = string.Empty; + private DatabaseReader? _dbReader; + private DateTime _dbLastWriteTime = DateTime.Now; + private CancellationTokenSource _fileWriteTimeCheckCts = new(); + private bool _processingReload = false; + + public GeoIPService(ILogger logger, + IConfigurationService mareConfiguration) + { + _logger = logger; + _mareConfiguration = mareConfiguration; + } + + public async Task GetCountryFromIP(IHttpContextAccessor httpContextAccessor) + { + if (!_useGeoIP) + { + return "*"; + } + + try + { + var ip = httpContextAccessor.GetIpAddress(); + + using CancellationTokenSource waitCts = new(); + waitCts.CancelAfter(TimeSpan.FromSeconds(5)); + while (_processingReload) await Task.Delay(100, waitCts.Token).ConfigureAwait(false); + + if (_dbReader.TryCountry(ip, out var response)) + { + return response.Continent.Code; + } + + return "*"; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error handling Geo IP country in request"); + return "*"; + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("GeoIP module starting update task"); + + var token = _fileWriteTimeCheckCts.Token; + _ = PeriodicReloadTask(token); + + return Task.CompletedTask; + } + + private async Task PeriodicReloadTask(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + _processingReload = true; + + var useGeoIP = _mareConfiguration.GetValueOrDefault(nameof(ServerConfiguration.UseGeoIP), false); + var countryFile = _mareConfiguration.GetValueOrDefault(nameof(ServerConfiguration.GeoIPDbCountryFile), string.Empty); + var lastWriteTime = new FileInfo(countryFile).LastWriteTimeUtc; + if (useGeoIP && (!string.Equals(countryFile, _countryFile, StringComparison.OrdinalIgnoreCase) || lastWriteTime != _dbLastWriteTime)) + { + _countryFile = countryFile; + if (!File.Exists(_countryFile)) throw new FileNotFoundException($"Could not open GeoIP Country Database, path does not exist: {_countryFile}"); + _dbReader?.Dispose(); + _dbReader = null; + _dbReader = new DatabaseReader(_countryFile); + _dbLastWriteTime = lastWriteTime; + + _ = _dbReader.Country("8.8.8.8").Continent; + + _logger.LogInformation($"Loaded GeoIP country file from {_countryFile}"); + + if (_useGeoIP != useGeoIP) + { + _logger.LogInformation("GeoIP module is now enabled"); + _useGeoIP = useGeoIP; + } + } + + if (_useGeoIP != useGeoIP && !useGeoIP) + { + _logger.LogInformation("GeoIP module is now disabled"); + _useGeoIP = useGeoIP; + } + } + catch (Exception e) + { + _logger.LogWarning(e, "Error during periodic GeoIP module reload task, disabling GeoIP"); + _useGeoIP = false; + } + finally + { + _processingReload = false; + } + + await Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _fileWriteTimeCheckCts.Cancel(); + _fileWriteTimeCheckCts.Dispose(); + _dbReader.Dispose(); + return Task.CompletedTask; + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Startup.cs b/MareSynchronosServer/MareSynchronosServer/Startup.cs index 8f3d499..efa3372 100644 --- a/MareSynchronosServer/MareSynchronosServer/Startup.cs +++ b/MareSynchronosServer/MareSynchronosServer/Startup.cs @@ -96,8 +96,10 @@ public class Startup if (isMainServer) { + services.AddSingleton(); services.AddSingleton(); services.AddHostedService(provider => provider.GetService()); + services.AddHostedService(provider => provider.GetService()); } } diff --git a/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs index 24c9522..579c4fe 100644 --- a/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs +++ b/MareSynchronosServer/MareSynchronosShared/Services/MareConfigurationServiceServer.cs @@ -1,5 +1,6 @@ using MareSynchronosShared.Utils; using Microsoft.Extensions.Options; +using System.Collections; using System.Text; namespace MareSynchronosShared.Services; @@ -30,11 +31,20 @@ public class MareConfigurationServiceServer : IConfigurationService where StringBuilder sb = new(); foreach (var prop in props) { - sb.AppendLine($"{prop.Name} (IsRemote: {prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), true).Any()}) => {prop.GetValue(_config.CurrentValue)}"); + var isRemote = prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), true).Any(); + var getValueMethod = GetType().GetMethod(nameof(GetValue)).MakeGenericMethod(prop.PropertyType); + var value = isRemote ? getValueMethod.Invoke(this, new[] { prop.Name }) : prop.GetValue(_config.CurrentValue); + if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && !typeof(string).IsAssignableFrom(prop.PropertyType)) + { + var enumVal = (IEnumerable)value; + value = string.Empty; + foreach (var listVal in enumVal) + { + value += listVal.ToString() + ", "; + } + } + sb.AppendLine($"{prop.Name} (IsRemote: {isRemote}) => {value}"); } - - sb.AppendLine(_config.ToString()); - return sb.ToString(); } } diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/CdnShardConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/CdnShardConfiguration.cs index 4b9d694..4ffe680 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/CdnShardConfiguration.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/CdnShardConfiguration.cs @@ -2,11 +2,12 @@ public class CdnShardConfiguration { + public List Continents { get; set; } public string FileMatch { get; set; } public Uri CdnFullUrl { get; set; } public override string ToString() { - return CdnFullUrl.ToString() + " == " + FileMatch; + return CdnFullUrl.ToString() + "[" + string.Join(',', Continents) + "] == " + FileMatch; } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/MareClaimTypes.cs b/MareSynchronosServer/MareSynchronosShared/Utils/MareClaimTypes.cs index 08539f4..dd2e747 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/MareClaimTypes.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/MareClaimTypes.cs @@ -5,4 +5,5 @@ public static class MareClaimTypes public const string Uid = "uid"; public const string CharaIdent = "character_identification"; public const string Internal = "internal"; + public const string Continent = "continent"; } diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationAuthBase.cs b/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationAuthBase.cs index b9683d0..a91c24e 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationAuthBase.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationAuthBase.cs @@ -14,6 +14,8 @@ public class MareConfigurationAuthBase : MareConfigurationBase public int RegisterIpDurationInMinutes { get; set; } = 10; [RemoteConfiguration] public List WhitelistedIps { get; set; } = new(); + [RemoteConfiguration] + public bool UseGeoIP { get; set; } = false; public override string ToString() { @@ -25,6 +27,7 @@ public class MareConfigurationAuthBase : MareConfigurationBase sb.AppendLine($"{nameof(RegisterIpDurationInMinutes)} => {RegisterIpDurationInMinutes}"); sb.AppendLine($"{nameof(Jwt)} => {Jwt}"); sb.AppendLine($"{nameof(WhitelistedIps)} => {string.Join(", ", WhitelistedIps)}"); + sb.AppendLine($"{nameof(UseGeoIP)} => {UseGeoIP}"); return sb.ToString(); } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/ServerConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/ServerConfiguration.cs index 2b8d668..8f54d0c 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/ServerConfiguration.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/ServerConfiguration.cs @@ -24,6 +24,7 @@ public class ServerConfiguration : MareConfigurationAuthBase [RemoteConfiguration] public int PurgeUnusedAccountsPeriodInDays { get; set; } = 14; + public string GeoIPDbCountryFile { get; set; } = string.Empty; public int RedisPool { get; set; } = 50; @@ -40,6 +41,7 @@ public class ServerConfiguration : MareConfigurationAuthBase sb.AppendLine($"{nameof(MaxGroupUserCount)} => {MaxGroupUserCount}"); sb.AppendLine($"{nameof(PurgeUnusedAccounts)} => {PurgeUnusedAccounts}"); sb.AppendLine($"{nameof(PurgeUnusedAccountsPeriodInDays)} => {PurgeUnusedAccountsPeriodInDays}"); + sb.AppendLine($"{nameof(GeoIPDbCountryFile)} => {GeoIPDbCountryFile}"); return sb.ToString(); } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/StaticFilesServerConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/StaticFilesServerConfiguration.cs index 7404e8a..658af57 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/StaticFilesServerConfiguration.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/StaticFilesServerConfiguration.cs @@ -29,6 +29,7 @@ public class StaticFilesServerConfiguration : MareConfigurationBase sb.AppendLine($"{nameof(CacheDirectory)} => {CacheDirectory}"); sb.AppendLine($"{nameof(DownloadQueueSize)} => {DownloadQueueSize}"); sb.AppendLine($"{nameof(DownloadQueueReleaseSeconds)} => {DownloadQueueReleaseSeconds}"); + sb.AppendLine($"{nameof(CdnShardConfiguration)} => {string.Join(", ", CdnShardConfiguration)}"); return sb.ToString(); } } diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ControllerBase.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ControllerBase.cs index 4ccbd64..630a741 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ControllerBase.cs +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ControllerBase.cs @@ -13,4 +13,5 @@ public class ControllerBase : Controller } protected string MareUser => HttpContext.User.Claims.First(f => string.Equals(f.Type, MareClaimTypes.Uid, StringComparison.Ordinal)).Value; + protected string Continent => HttpContext.User.Claims.FirstOrDefault(f => string.Equals(f.Type, MareClaimTypes.Continent, StringComparison.Ordinal))?.Value ?? "*"; } diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs index f35fa5e..66ce9f8 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Controllers/ServerFilesController.cs @@ -36,7 +36,7 @@ public class ServerFilesController : ControllerBase public ServerFilesController(ILogger logger, CachedFileProvider cachedFileProvider, IConfigurationService configuration, - IHubContext hubContext, + IHubContext hubContext, MareDbContext mareDbContext, MareMetrics metricsClient) : base(logger) { _basePath = configuration.GetValue(nameof(StaticFilesServerConfiguration.CacheDirectory)); @@ -79,14 +79,35 @@ public class ServerFilesController : ControllerBase var cacheFile = await _mareDbContext.Files.AsNoTracking().Where(f => hashes.Contains(f.Hash)).AsNoTracking().Select(k => new { k.Hash, k.Size }).AsNoTracking().ToListAsync().ConfigureAwait(false); - var shardConfig = new List(_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CdnShardConfiguration), new List())); + var allFileShards = new List(_configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.CdnShardConfiguration), new List())); foreach (var file in cacheFile) { var forbiddenFile = forbiddenFiles.SingleOrDefault(f => string.Equals(f.Hash, file.Hash, StringComparison.OrdinalIgnoreCase)); + Uri? baseUrl = null; - var matchedShardConfig = shardConfig.OrderBy(g => Guid.NewGuid()).FirstOrDefault(f => new Regex(f.FileMatch).IsMatch(file.Hash)); - var baseUrl = matchedShardConfig?.CdnFullUrl ?? _configuration.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)); + if (forbiddenFile == null) + { + List selectedShards = new(); + var matchingShards = allFileShards.Where(f => new Regex(f.FileMatch).IsMatch(file.Hash)).ToList(); + + if (string.Equals(Continent, "*", StringComparison.Ordinal)) + { + selectedShards = matchingShards; + } + else + { + selectedShards = matchingShards.Where(c => c.Continents.Contains(Continent, StringComparer.OrdinalIgnoreCase)).ToList(); + if (!selectedShards.Any()) selectedShards = matchingShards; + } + + var shard = selectedShards + .OrderBy(s => !s.Continents.Any() ? 0 : 1) + .ThenBy(s => s.Continents.Contains("*", StringComparer.Ordinal) ? 0 : 1) + .ThenBy(g => Guid.NewGuid()).FirstOrDefault(); + + baseUrl = shard?.CdnFullUrl ?? _configuration.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)); + } response.Add(new DownloadFileDto { @@ -95,7 +116,7 @@ public class ServerFilesController : ControllerBase IsForbidden = forbiddenFile != null, Hash = file.Hash, Size = file.Size, - Url = baseUrl.ToString(), + Url = baseUrl?.ToString() ?? string.Empty, }); }