rework server responsibilities (#18)
* rework server responsibilities add remote configuration * start metrics only when compiled as not debug * add some more logging to discord bot * fixes of some casts * make metrics port configurable, minor fixes * add docker bullshit * md formatting * adjustments to docker stuff * fix docker json files, fix some stuff in discord bot, add /useradd for Discord bot * adjust docker configs and fix sharded.bat * fixes for logs, cache file provider repeat trying to open filestream Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com>
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronosShared.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
@@ -11,6 +8,7 @@ namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
// TODO: remove all of this and migrate it to the discord bot eventually
|
||||
private List<string> OnlineAdmins => _dbContext.Users.Where(u => (u.IsModerator || u.IsAdmin)).Select(u => u.UID).ToList();
|
||||
|
||||
[Authorize(Policy = "Admin")]
|
||||
@@ -116,11 +114,11 @@ public partial class MareHub
|
||||
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Clients.Users(OnlineAdmins).Client_AdminUpdateOrAddBannedUser(dto).ConfigureAwait(false);
|
||||
var bannedUser = _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash);
|
||||
if (!string.IsNullOrEmpty(bannedUser))
|
||||
{
|
||||
await Clients.User(bannedUser).Client_AdminForcedReconnect().ConfigureAwait(false);
|
||||
}
|
||||
//var bannedUser = _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash);
|
||||
//if (!string.IsNullOrEmpty(bannedUser))
|
||||
//{
|
||||
// await Clients.User(bannedUser).Client_AdminForcedReconnect().ConfigureAwait(false);
|
||||
//}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Admin")]
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using MareSynchronos.API;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
|
||||
namespace MareSynchronosServer.Hubs
|
||||
{
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Core;
|
||||
using MareSynchronos.API;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
using MareSynchronosShared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using MareSynchronosServer.Utils;
|
||||
using System.Security.Claims;
|
||||
|
||||
|
||||
@@ -5,11 +5,7 @@ using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.RegularExpressions;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared.Metrics;
|
||||
@@ -72,7 +68,7 @@ public partial class MareHub
|
||||
var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
|
||||
|
||||
var usersToSendOnlineTo = await SendOnlineToAllPairedUsers(ownIdent).ConfigureAwait(false);
|
||||
return usersToSendOnlineTo.Select(e => _clientIdentService.GetCharacterIdentForUid(e)).Where(t => !string.IsNullOrEmpty(t)).Distinct(System.StringComparer.Ordinal).ToList();
|
||||
return usersToSendOnlineTo.Select(e => _clientIdentService.GetCharacterIdentForUid(e)).Where(t => !string.IsNullOrEmpty(t)).Distinct(StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
@@ -152,8 +148,8 @@ public partial class MareHub
|
||||
|
||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
|
||||
var allPairedUsersDict = allPairedUsers.ToDictionary(f => f, f => _clientIdentService.GetCharacterIdentForUid(f), System.StringComparer.Ordinal)
|
||||
.Where(f => visibleCharacterIds.Contains(f.Value, System.StringComparer.Ordinal));
|
||||
var allPairedUsersDict = allPairedUsers.ToDictionary(f => f, f => _clientIdentService.GetCharacterIdentForUid(f), StringComparer.Ordinal)
|
||||
.Where(f => visibleCharacterIds.Contains(f.Value, StringComparer.Ordinal));
|
||||
|
||||
var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
|
||||
|
||||
@@ -172,7 +168,7 @@ public partial class MareHub
|
||||
|
||||
// don't allow adding yourself or nothing
|
||||
uid = uid.Trim();
|
||||
if (string.Equals(uid, AuthenticatedUserId, System.StringComparison.Ordinal) || string.IsNullOrWhiteSpace(uid)) return;
|
||||
if (string.Equals(uid, AuthenticatedUserId, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(uid)) return;
|
||||
|
||||
// grab other user, check if it exists and if a pair already exists
|
||||
var otherUser = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false);
|
||||
@@ -231,7 +227,7 @@ public partial class MareHub
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
|
||||
// if the other user has paused the main user and there was no previous group connection don't send anything
|
||||
if (!otherEntry.IsPaused && allUserPairs.Any(p => string.Equals(p.UID, uid, System.StringComparison.Ordinal) && p.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection))
|
||||
if (!otherEntry.IsPaused && allUserPairs.Any(p => string.Equals(p.UID, uid, StringComparison.Ordinal) && p.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection))
|
||||
{
|
||||
await Clients.User(user.UID).Client_UserChangePairedPlayer(otherIdent, true).ConfigureAwait(false);
|
||||
await Clients.User(otherUser.UID).Client_UserChangePairedPlayer(userIdent, true).ConfigureAwait(false);
|
||||
@@ -243,7 +239,7 @@ public partial class MareHub
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(otherUserUid, isPaused));
|
||||
|
||||
if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return;
|
||||
if (string.Equals(otherUserUid, AuthenticatedUserId, StringComparison.Ordinal)) return;
|
||||
ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false);
|
||||
if (pair == null) return;
|
||||
|
||||
@@ -288,7 +284,7 @@ public partial class MareHub
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(otherUserUid));
|
||||
|
||||
if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return;
|
||||
if (string.Equals(otherUserUid, AuthenticatedUserId, StringComparison.Ordinal)) return;
|
||||
|
||||
// check if client pair even exists
|
||||
ClientPair callerPair =
|
||||
@@ -331,7 +327,7 @@ public partial class MareHub
|
||||
if (!callerHadPaused && otherHadPaused) return;
|
||||
|
||||
var allUsers = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
var pauseEntry = allUsers.SingleOrDefault(f => string.Equals(f.UID, otherUserUid, System.StringComparison.Ordinal));
|
||||
var pauseEntry = allUsers.SingleOrDefault(f => string.Equals(f.UID, otherUserUid, StringComparison.Ordinal));
|
||||
var isPausedInGroup = pauseEntry == null || pauseEntry.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection;
|
||||
|
||||
// if neither user had paused each other and both are in unpaused groups, state will be online for both, do nothing
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using System.Security.Claims;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronosServer.Services;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Protos;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
@@ -23,7 +20,7 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
|
||||
private readonly FileService.FileServiceClient _fileServiceClient;
|
||||
private readonly SystemInfoService _systemInfoService;
|
||||
private readonly IHttpContextAccessor _contextAccessor;
|
||||
private readonly GrpcClientIdentificationService _clientIdentService;
|
||||
private readonly IClientIdentificationService _clientIdentService;
|
||||
private readonly MareHubLogger _logger;
|
||||
private readonly MareDbContext _dbContext;
|
||||
private readonly Uri _cdnFullUri;
|
||||
@@ -33,18 +30,18 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
|
||||
private readonly int _maxGroupUserCount;
|
||||
|
||||
public MareHub(MareMetrics mareMetrics, FileService.FileServiceClient fileServiceClient,
|
||||
MareDbContext mareDbContext, ILogger<MareHub> logger, SystemInfoService systemInfoService, IOptions<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
|
||||
GrpcClientIdentificationService clientIdentService)
|
||||
MareDbContext mareDbContext, ILogger<MareHub> logger, SystemInfoService systemInfoService,
|
||||
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
|
||||
IClientIdentificationService clientIdentService)
|
||||
{
|
||||
_mareMetrics = mareMetrics;
|
||||
_fileServiceClient = fileServiceClient;
|
||||
_systemInfoService = systemInfoService;
|
||||
var config = configuration.Value;
|
||||
_cdnFullUri = config.CdnFullUrl;
|
||||
_shardName = config.ShardName;
|
||||
_maxExistingGroupsByUser = config.MaxExistingGroupsByUser;
|
||||
_maxJoinedGroupsByUser = config.MaxJoinedGroupsByUser;
|
||||
_maxGroupUserCount = config.MaxGroupUserCount;
|
||||
_cdnFullUri = configuration.GetValue<Uri>(nameof(ServerConfiguration.CdnFullUrl));
|
||||
_shardName = configuration.GetValue<string>(nameof(ServerConfiguration.ShardName));
|
||||
_maxExistingGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3);
|
||||
_maxJoinedGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxJoinedGroupsByUser), 6);
|
||||
_maxGroupUserCount = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 100);
|
||||
_contextAccessor = contextAccessor;
|
||||
_clientIdentService = clientIdentService;
|
||||
_logger = new MareHubLogger(this, logger);
|
||||
@@ -109,14 +106,14 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public async Task<bool> CheckClientHealth()
|
||||
public Task<bool> CheckClientHealth()
|
||||
{
|
||||
var needsReconnect = !_clientIdentService.IsOnCurrentServer(AuthenticatedUserId);
|
||||
if (needsReconnect)
|
||||
{
|
||||
_logger.LogCallWarning(MareHubLogger.Args(needsReconnect));
|
||||
}
|
||||
return needsReconnect;
|
||||
return Task.FromResult(needsReconnect);
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Security.Claims;
|
||||
using AspNetCoreRateLimit;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using MareSynchronosShared;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
public class SignalRLimitFilter : IHubFilter
|
||||
{
|
||||
private readonly IRateLimitProcessor _processor;
|
||||
@@ -1,16 +1,12 @@
|
||||
using MareSynchronosShared.Protos;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MareSynchronosServices.Identity;
|
||||
namespace MareSynchronosServer.Identity;
|
||||
|
||||
public class IdentityHandler
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ServerIdentity> cachedIdentities = new();
|
||||
private readonly ConcurrentDictionary<string, ConcurrentQueue<IdentChange>> identChanges = new();
|
||||
private readonly ConcurrentDictionary<string, ServerIdentity> _cachedIdentities = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ConcurrentQueue<IdentChange>> _identChanges = new(StringComparer.Ordinal);
|
||||
private readonly ILogger<IdentityHandler> _logger;
|
||||
|
||||
public IdentityHandler(ILogger<IdentityHandler> logger)
|
||||
@@ -18,16 +14,9 @@ public class IdentityHandler
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
internal Task<string> GetUidForCharacterIdent(string ident, string serverId)
|
||||
internal Task<ServerIdentity> GetIdentForUid(string uid)
|
||||
{
|
||||
var exists = cachedIdentities.Any(f => f.Value.CharacterIdent == ident && f.Value.ServerId == serverId);
|
||||
return Task.FromResult(exists ? cachedIdentities.FirstOrDefault(f => f.Value.CharacterIdent == ident && f.Value.ServerId == serverId).Key : string.Empty);
|
||||
}
|
||||
|
||||
internal Task<ServerIdentity> GetIdentForuid(string uid)
|
||||
{
|
||||
ServerIdentity result;
|
||||
if (!cachedIdentities.TryGetValue(uid, out result))
|
||||
if (!_cachedIdentities.TryGetValue(uid, out ServerIdentity result))
|
||||
{
|
||||
result = new ServerIdentity();
|
||||
}
|
||||
@@ -37,40 +26,40 @@ public class IdentityHandler
|
||||
|
||||
internal void SetIdent(string uid, string serverId, string ident)
|
||||
{
|
||||
cachedIdentities[uid] = new ServerIdentity() { ServerId = serverId, CharacterIdent = ident };
|
||||
_cachedIdentities[uid] = new ServerIdentity() { ServerId = serverId, CharacterIdent = ident };
|
||||
}
|
||||
|
||||
internal void RemoveIdent(string uid, string serverId)
|
||||
{
|
||||
if (cachedIdentities.ContainsKey(uid) && cachedIdentities[uid].ServerId == serverId)
|
||||
if (_cachedIdentities.ContainsKey(uid) && string.Equals(_cachedIdentities[uid].ServerId, serverId, StringComparison.Ordinal))
|
||||
{
|
||||
cachedIdentities.TryRemove(uid, out _);
|
||||
_cachedIdentities.TryRemove(uid, out _);
|
||||
}
|
||||
}
|
||||
|
||||
internal int GetOnlineUsers(string serverId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(serverId))
|
||||
return cachedIdentities.Count;
|
||||
return cachedIdentities.Count(c => c.Value.ServerId == serverId);
|
||||
return _cachedIdentities.Count;
|
||||
return _cachedIdentities.Count(c => string.Equals(c.Value.ServerId, serverId, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
internal Dictionary<string, ServerIdentity> GetIdentsForAllExcept(string serverId)
|
||||
{
|
||||
return cachedIdentities.Where(k => k.Value.ServerId != serverId).ToDictionary(k => k.Key, k => k.Value);
|
||||
return _cachedIdentities.Where(k => !string.Equals(k.Value.ServerId, serverId, StringComparison.Ordinal)).ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
internal Dictionary<string, ServerIdentity> GetIdentsForServer(string serverId)
|
||||
{
|
||||
return cachedIdentities.Where(k => k.Value.ServerId == serverId).ToDictionary(k => k.Key, k => k.Value);
|
||||
return _cachedIdentities.Where(k => string.Equals(k.Value.ServerId, serverId, StringComparison.Ordinal)).ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
internal void ClearIdentsForServer(string serverId)
|
||||
{
|
||||
var serverIdentities = cachedIdentities.Where(i => i.Value.ServerId == serverId);
|
||||
var serverIdentities = _cachedIdentities.Where(i => string.Equals(i.Value.ServerId, serverId, StringComparison.Ordinal));
|
||||
foreach (var identity in serverIdentities)
|
||||
{
|
||||
cachedIdentities.TryRemove(identity.Key, out _);
|
||||
_cachedIdentities.TryRemove(identity.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,16 +67,16 @@ public class IdentityHandler
|
||||
{
|
||||
_logger.LogInformation("Enqueued " + identchange.UidWithIdent.Uid.Uid + ":" + identchange.IsOnline + " from " + identchange.UidWithIdent.Ident.ServerId);
|
||||
|
||||
foreach (var k in identChanges.Keys)
|
||||
foreach (var k in _identChanges.Keys)
|
||||
{
|
||||
if (string.Equals(k, identchange.UidWithIdent.Ident.ServerId, System.StringComparison.Ordinal)) continue;
|
||||
identChanges[k].Enqueue(identchange);
|
||||
if (string.Equals(k, identchange.UidWithIdent.Ident.ServerId, StringComparison.Ordinal)) continue;
|
||||
_identChanges[k].Enqueue(identchange);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool DequeueIdentChange(string server, out IdentChange? cur)
|
||||
internal bool DequeueIdentChange(string server, out IdentChange cur)
|
||||
{
|
||||
if (!(identChanges.ContainsKey(server) && identChanges[server].TryDequeue(out cur)))
|
||||
if (!(_identChanges.ContainsKey(server) && _identChanges[server].TryDequeue(out cur)))
|
||||
{
|
||||
cur = null;
|
||||
return false;
|
||||
@@ -98,7 +87,7 @@ public class IdentityHandler
|
||||
|
||||
internal void RegisterServerForQueue(string serverId)
|
||||
{
|
||||
identChanges[serverId] = new ConcurrentQueue<IdentChange>();
|
||||
_identChanges[serverId] = new ConcurrentQueue<IdentChange>();
|
||||
}
|
||||
|
||||
internal record ServerIdentity
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<UserSecretsId>aspnet-MareSynchronosServer-BA82A12A-0B30-463C-801D-B7E81318CD50</UserSecretsId>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
|
||||
namespace MareSynchronosServer;
|
||||
|
||||
@@ -22,9 +16,12 @@ public class Program
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
using var context = services.GetRequiredService<MareDbContext>();
|
||||
var options = services.GetRequiredService<IConfigurationService<ServerConfiguration>>();
|
||||
var logger = host.Services.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogInformation("Loaded MareSynchronos Server Configuration (IsMain: {isMain})", options.IsMain);
|
||||
logger.LogInformation(options.ToString());
|
||||
|
||||
var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER");
|
||||
if (string.IsNullOrEmpty(secondaryServer) || string.Equals(secondaryServer, "0", StringComparison.Ordinal))
|
||||
if (options.IsMain)
|
||||
{
|
||||
context.Database.Migrate();
|
||||
context.SaveChanges();
|
||||
@@ -42,15 +39,18 @@ public class Program
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.Count());
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.Count(p => p.IsPaused));
|
||||
|
||||
var options = host.Services.GetService<IOptions<ServerConfiguration>>();
|
||||
var logger = host.Services.GetService<ILogger<Program>>();
|
||||
logger.LogInformation("Loaded MareSynchronos Server Configuration");
|
||||
logger.LogInformation(options.Value.ToString());
|
||||
}
|
||||
|
||||
if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal))
|
||||
{
|
||||
host.Run();
|
||||
try
|
||||
{
|
||||
host.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MareSynchronosShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronosServer.Services;
|
||||
|
||||
namespace MareSynchronosServer.RequirementHandlers;
|
||||
|
||||
public class UserRequirementHandler : AuthorizationHandler<UserRequirement, HubInvocationContext>
|
||||
{
|
||||
private readonly GrpcClientIdentificationService identClient;
|
||||
private readonly IClientIdentificationService identClient;
|
||||
private readonly MareDbContext dbContext;
|
||||
private readonly ILogger<UserRequirementHandler> logger;
|
||||
|
||||
public UserRequirementHandler(GrpcClientIdentificationService identClient, MareDbContext dbContext, ILogger<UserRequirementHandler> logger)
|
||||
public UserRequirementHandler(IClientIdentificationService identClient, MareDbContext dbContext, ILogger<UserRequirementHandler> logger)
|
||||
{
|
||||
this.identClient = identClient;
|
||||
this.dbContext = dbContext;
|
||||
|
||||
@@ -2,17 +2,12 @@
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Protos;
|
||||
using MareSynchronosShared.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using MareSynchronosShared.Utils;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
public class GrpcClientIdentificationService : GrpcBaseService
|
||||
public class GrpcClientIdentificationService : GrpcBaseService, IClientIdentificationService
|
||||
{
|
||||
private readonly string _shardName;
|
||||
private readonly ILogger<GrpcClientIdentificationService> _logger;
|
||||
@@ -22,13 +17,15 @@ public class GrpcClientIdentificationService : GrpcBaseService
|
||||
private readonly MareMetrics _metrics;
|
||||
protected readonly ConcurrentDictionary<string, UidWithIdent> OnlineClients = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, UidWithIdent> RemoteCachedIdents = new(StringComparer.Ordinal);
|
||||
private ConcurrentQueue<IdentChange> _identChangeQueue = new();
|
||||
private readonly ConcurrentQueue<IdentChange> _identChangeQueue = new();
|
||||
|
||||
public GrpcClientIdentificationService(ILogger<GrpcClientIdentificationService> logger, IdentificationService.IdentificationServiceClient gprcIdentClient,
|
||||
public GrpcClientIdentificationService(ILogger<GrpcClientIdentificationService> logger,
|
||||
IdentificationService.IdentificationServiceClient gprcIdentClient,
|
||||
IdentificationService.IdentificationServiceClient gprcIdentClientStreamOut,
|
||||
IdentificationService.IdentificationServiceClient gprcIdentClientStreamIn, MareMetrics metrics, IOptions<ServerConfiguration> configuration) : base(logger)
|
||||
IdentificationService.IdentificationServiceClient gprcIdentClientStreamIn,
|
||||
MareMetrics metrics, IConfigurationService<ServerConfiguration> configuration) : base(logger)
|
||||
{
|
||||
_shardName = configuration.Value.ShardName;
|
||||
_shardName = configuration.GetValueOrDefault(nameof(ServerConfiguration.ShardName), string.Empty);
|
||||
_logger = logger;
|
||||
_grpcIdentClient = gprcIdentClient;
|
||||
_grpcIdentClientStreamOut = gprcIdentClientStreamOut;
|
||||
@@ -171,7 +168,7 @@ public class GrpcClientIdentificationService : GrpcBaseService
|
||||
using var stream = _grpcIdentClientStreamIn.ReceiveStreamIdentStatusChange(new ServerMessage()
|
||||
{
|
||||
ServerId = _shardName,
|
||||
});
|
||||
}, cancellationToken: cts);
|
||||
_logger.LogInformation("Starting Receive Online Client Data stream");
|
||||
await foreach (var cur in stream.ResponseStream.ReadAllAsync(cts).ConfigureAwait(false))
|
||||
{
|
||||
@@ -201,7 +198,7 @@ public class GrpcClientIdentificationService : GrpcBaseService
|
||||
|
||||
protected override async Task StopAsyncInternal(CancellationToken cancellationToken)
|
||||
{
|
||||
await ExecuteOnGrpc(_grpcIdentClient.ClearIdentsForServerAsync(new ServerMessage() { ServerId = _shardName })).ConfigureAwait(false);
|
||||
await ExecuteOnGrpc(_grpcIdentClient.ClearIdentsForServerAsync(new ServerMessage() { ServerId = _shardName }, cancellationToken: cancellationToken)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override async Task OnGrpcRestore()
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
public interface IClientIdentificationService : IHostedService
|
||||
{
|
||||
string GetCharacterIdentForUid(string uid);
|
||||
Task<long> GetOnlineUsers();
|
||||
string GetServerForUid(string uid);
|
||||
bool IsOnCurrentServer(string uid);
|
||||
void MarkUserOffline(string uid);
|
||||
void MarkUserOnline(string uid, string charaIdent);
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
using Grpc.Core;
|
||||
using MareSynchronosServer.Identity;
|
||||
using MareSynchronosShared.Protos;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace MareSynchronosServices.Identity;
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
internal class IdentityService : IdentificationService.IdentificationServiceBase
|
||||
internal class GrpcIdentityService : IdentificationService.IdentificationServiceBase
|
||||
{
|
||||
private readonly ILogger<IdentityService> _logger;
|
||||
private readonly ILogger<GrpcIdentityService> _logger;
|
||||
private readonly IdentityHandler _handler;
|
||||
|
||||
public IdentityService(ILogger<IdentityService> logger, IdentityHandler handler)
|
||||
public GrpcIdentityService(ILogger<GrpcIdentityService> logger, IdentityHandler handler)
|
||||
{
|
||||
_logger = logger;
|
||||
_handler = handler;
|
||||
@@ -18,7 +18,7 @@ internal class IdentityService : IdentificationService.IdentificationServiceBase
|
||||
|
||||
public override async Task<CharacterIdentMessage> GetIdentForUid(UidMessage request, ServerCallContext context)
|
||||
{
|
||||
var result = await _handler.GetIdentForuid(request.Uid);
|
||||
var result = await _handler.GetIdentForUid(request.Uid).ConfigureAwait(false);
|
||||
return new CharacterIdentMessage()
|
||||
{
|
||||
Ident = result.CharacterIdent,
|
||||
@@ -26,6 +26,7 @@ internal class IdentityService : IdentificationService.IdentificationServiceBase
|
||||
};
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
public override Task<OnlineUserCountResponse> GetOnlineUserCount(ServerMessage request, ServerCallContext context)
|
||||
{
|
||||
return Task.FromResult(new OnlineUserCountResponse() { Count = _handler.GetOnlineUsers(request.ServerId) });
|
||||
@@ -66,7 +67,7 @@ internal class IdentityService : IdentificationService.IdentificationServiceBase
|
||||
|
||||
public override async Task<Empty> SendStreamIdentStatusChange(IAsyncStreamReader<IdentChangeMessage> requestStream, ServerCallContext context)
|
||||
{
|
||||
await requestStream.MoveNext();
|
||||
await requestStream.MoveNext().ConfigureAwait(false);
|
||||
var server = requestStream.Current.Server;
|
||||
if (server == null) throw new System.Exception("First message needs to be server message");
|
||||
_handler.RegisterServerForQueue(server.ServerId);
|
||||
@@ -0,0 +1,60 @@
|
||||
using MareSynchronosShared.Protos;
|
||||
using System.Collections.Concurrent;
|
||||
using MareSynchronosServer.Identity;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
public class LocalClientIdentificationService : IClientIdentificationService
|
||||
{
|
||||
protected readonly ConcurrentDictionary<string, UidWithIdent> OnlineClients = new(StringComparer.Ordinal);
|
||||
private readonly IdentityHandler _identityHandler;
|
||||
private readonly string _shardName;
|
||||
|
||||
public LocalClientIdentificationService(IdentityHandler identityHandler, IConfigurationService<ServerConfiguration> config)
|
||||
{
|
||||
_identityHandler = identityHandler;
|
||||
_shardName = config.GetValueOrDefault(nameof(ServerConfiguration.ShardName), string.Empty);
|
||||
}
|
||||
|
||||
public string GetCharacterIdentForUid(string uid)
|
||||
{
|
||||
return _identityHandler.GetIdentForUid(uid).Result.CharacterIdent;
|
||||
}
|
||||
|
||||
public Task<long> GetOnlineUsers()
|
||||
{
|
||||
return Task.FromResult((long)_identityHandler.GetOnlineUsers(string.Empty));
|
||||
}
|
||||
|
||||
public string GetServerForUid(string uid)
|
||||
{
|
||||
return _identityHandler.GetIdentForUid(uid).Result.ServerId;
|
||||
}
|
||||
|
||||
public bool IsOnCurrentServer(string uid)
|
||||
{
|
||||
return string.Equals(_identityHandler.GetIdentForUid(uid).Result.ServerId, _shardName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public void MarkUserOffline(string uid)
|
||||
{
|
||||
_identityHandler.RemoveIdent(uid, _shardName);
|
||||
}
|
||||
|
||||
public void MarkUserOnline(string uid, string charaIdent)
|
||||
{
|
||||
_identityHandler.SetIdent(uid, _shardName, charaIdent);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,30 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronosServer.Hubs;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
public class SystemInfoService : IHostedService, IDisposable
|
||||
{
|
||||
private readonly MareMetrics _mareMetrics;
|
||||
private readonly IConfigurationService<ServerConfiguration> _config;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly GrpcClientIdentificationService _clientIdentService;
|
||||
private readonly IClientIdentificationService _clientIdentService;
|
||||
private readonly ILogger<SystemInfoService> _logger;
|
||||
private readonly IHubContext<MareHub, IMareHub> _hubContext;
|
||||
private Timer _timer;
|
||||
public SystemInfoDto SystemInfoDto { get; private set; } = new();
|
||||
|
||||
public SystemInfoService(MareMetrics mareMetrics, IServiceProvider services, GrpcClientIdentificationService clientIdentService, ILogger<SystemInfoService> logger, IHubContext<MareHub, IMareHub> hubContext)
|
||||
public SystemInfoService(MareMetrics mareMetrics, IConfigurationService<ServerConfiguration> configurationService, IServiceProvider services,
|
||||
IClientIdentificationService clientIdentService, ILogger<SystemInfoService> logger, IHubContext<MareHub, IMareHub> hubContext)
|
||||
{
|
||||
_mareMetrics = mareMetrics;
|
||||
_config = configurationService;
|
||||
_services = services;
|
||||
_clientIdentService = clientIdentService;
|
||||
_logger = logger;
|
||||
@@ -49,14 +47,16 @@ public class SystemInfoService : IHostedService, IDisposable
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads);
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads);
|
||||
|
||||
var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER");
|
||||
if (string.IsNullOrEmpty(secondaryServer) || string.Equals(secondaryServer, "0", StringComparison.Ordinal))
|
||||
if (_config.IsMain)
|
||||
{
|
||||
var onlineUsers = (int)_clientIdentService.GetOnlineUsers().Result;
|
||||
SystemInfoDto = new SystemInfoDto()
|
||||
{
|
||||
OnlineUsers = (int)_clientIdentService.GetOnlineUsers().Result,
|
||||
OnlineUsers = onlineUsers,
|
||||
};
|
||||
|
||||
_logger.LogInformation("Sending System Info, Online Users: {onlineUsers}", onlineUsers);
|
||||
|
||||
_hubContext.Clients.All.Client_UpdateSystemInfo(SystemInfoDto);
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
|
||||
@@ -1,50 +1,114 @@
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Models;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MareSynchronosServices;
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
public class CleanupService : IHostedService, IDisposable
|
||||
public class UserCleanupService : IHostedService
|
||||
{
|
||||
private readonly MareMetrics metrics;
|
||||
private readonly ILogger<CleanupService> _logger;
|
||||
private readonly ILogger<UserCleanupService> _logger;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ServicesConfiguration _configuration;
|
||||
private Timer? _timer;
|
||||
private readonly IConfigurationService<ServerConfiguration> _configuration;
|
||||
private CancellationTokenSource _cleanupCts;
|
||||
|
||||
public CleanupService(MareMetrics metrics, ILogger<CleanupService> logger, IServiceProvider services, IOptions<ServicesConfiguration> configuration)
|
||||
public UserCleanupService(MareMetrics metrics, ILogger<UserCleanupService> logger, IServiceProvider services, IConfigurationService<ServerConfiguration> configuration)
|
||||
{
|
||||
this.metrics = metrics;
|
||||
_logger = logger;
|
||||
_services = services;
|
||||
_configuration = configuration.Value;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Cleanup Service started");
|
||||
_cleanupCts = new();
|
||||
|
||||
_timer = new Timer(CleanUp, null, TimeSpan.Zero, TimeSpan.FromMinutes(10));
|
||||
_ = CleanUp(_cleanupCts.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void CleanUp(object state)
|
||||
private async Task CleanUp(CancellationToken ct)
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
using var dbContext = scope.ServiceProvider.GetService<MareDbContext>()!;
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
using var dbContext = scope.ServiceProvider.GetService<MareDbContext>()!;
|
||||
|
||||
CleanUpOutdatedLodestoneAuths(dbContext);
|
||||
|
||||
await PurgeUnusedAccounts(dbContext).ConfigureAwait(false);
|
||||
|
||||
await PurgeTempInvites(dbContext).ConfigureAwait(false);
|
||||
|
||||
dbContext.SaveChanges();
|
||||
|
||||
var now = DateTime.Now;
|
||||
TimeOnly currentTime = new(now.Hour, now.Minute, now.Second);
|
||||
TimeOnly futureTime = new(now.Hour, now.Minute - now.Minute % 10, 0);
|
||||
var span = futureTime.AddMinutes(10) - currentTime;
|
||||
|
||||
_logger.LogInformation("User Cleanup Complete, next run at {date}", now.Add(span));
|
||||
await Task.Delay(span, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PurgeTempInvites(MareDbContext dbContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempInvites = await dbContext.GroupTempInvites.ToListAsync().ConfigureAwait(false);
|
||||
dbContext.RemoveRange(tempInvites.Where(i => i.ExpirationDate < DateTime.UtcNow));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during Temp Invite purge");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PurgeUnusedAccounts(MareDbContext dbContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_configuration.GetValueOrDefault(nameof(ServerConfiguration.PurgeUnusedAccounts), false))
|
||||
{
|
||||
var usersOlderThanDays = _configuration.GetValueOrDefault(nameof(ServerConfiguration.PurgeUnusedAccountsPeriodInDays), 14);
|
||||
var maxGroupsByUser = _configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3);
|
||||
|
||||
_logger.LogInformation("Cleaning up users older than {usersOlderThanDays} days", usersOlderThanDays);
|
||||
|
||||
var allUsers = dbContext.Users.Where(u => string.IsNullOrEmpty(u.Alias)).ToList();
|
||||
List<User> usersToRemove = new();
|
||||
foreach (var user in allUsers)
|
||||
{
|
||||
if (user.LastLoggedIn < DateTime.UtcNow - TimeSpan.FromDays(usersOlderThanDays))
|
||||
{
|
||||
_logger.LogInformation("User outdated: {userUID}", user.UID);
|
||||
usersToRemove.Add(user);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var user in usersToRemove)
|
||||
{
|
||||
await SharedDbFunctions.PurgeUser(_logger, user, dbContext, maxGroupsByUser).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Cleaning up unauthorized users");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during user purge");
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanUpOutdatedLodestoneAuths(MareDbContext dbContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"Cleaning up expired lodestone authentications");
|
||||
@@ -65,52 +129,6 @@ public class CleanupService : IHostedService, IDisposable
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during expired auths cleanup");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_configuration.PurgeUnusedAccounts)
|
||||
{
|
||||
var usersOlderThanDays = _configuration.PurgeUnusedAccountsPeriodInDays;
|
||||
|
||||
_logger.LogInformation("Cleaning up users older than {usersOlderThanDays} days", usersOlderThanDays);
|
||||
|
||||
var allUsers = dbContext.Users.Where(u => string.IsNullOrEmpty(u.Alias)).ToList();
|
||||
List<User> usersToRemove = new();
|
||||
foreach (var user in allUsers)
|
||||
{
|
||||
if (user.LastLoggedIn < (DateTime.UtcNow - TimeSpan.FromDays(usersOlderThanDays)))
|
||||
{
|
||||
_logger.LogInformation("User outdated: {userUID}", user.UID);
|
||||
usersToRemove.Add(user);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var user in usersToRemove)
|
||||
{
|
||||
await PurgeUser(user, dbContext);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Cleaning up unauthorized users");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during user purge");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tempInvites = await dbContext.GroupTempInvites.ToListAsync();
|
||||
dbContext.RemoveRange(tempInvites.Where(i => i.ExpirationDate < DateTime.UtcNow));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during Temp Invite purge");
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Cleanup complete");
|
||||
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
public async Task PurgeUser(User user, MareDbContext dbContext)
|
||||
@@ -152,13 +170,13 @@ public class CleanupService : IHostedService, IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = await SharedDbFunctions.MigrateOrDeleteGroup(dbContext, userGroupPair.Group, groupPairs, _configuration.MaxExistingGroupsByUser).ConfigureAwait(false);
|
||||
_ = await SharedDbFunctions.MigrateOrDeleteGroup(dbContext, userGroupPair.Group, groupPairs, _configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
dbContext.GroupPairs.Remove(userGroupPair);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogInformation("User purged: {uid}", user.UID);
|
||||
@@ -166,20 +184,15 @@ public class CleanupService : IHostedService, IDisposable
|
||||
dbContext.Auth.Remove(auth);
|
||||
dbContext.Users.Remove(user);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
metrics.DecGauge(MetricsAPI.GaugeUsersRegistered, 1);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
_cleanupCts.Cancel();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
using System;
|
||||
using MareSynchronos.API;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using MareSynchronosServer.Hubs;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
@@ -16,15 +10,14 @@ using MareSynchronosShared.Authentication;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Protos;
|
||||
using Grpc.Net.Client.Configuration;
|
||||
using Prometheus;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using System.Collections.Generic;
|
||||
using MareSynchronosServer.Services;
|
||||
using System.Net.Http;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosServer.RequirementHandlers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronosShared.Utils;
|
||||
using MareSynchronosServer.Identity;
|
||||
using MareSynchronosShared.Services;
|
||||
using Prometheus;
|
||||
|
||||
namespace MareSynchronosServer;
|
||||
|
||||
@@ -41,98 +34,91 @@ public class Startup
|
||||
{
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.Configure<ServerConfiguration>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
services.Configure<MareConfigurationAuthBase>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
|
||||
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
|
||||
services.AddTransient(_ => Configuration);
|
||||
|
||||
services.AddMemoryCache();
|
||||
services.AddInMemoryRateLimiting();
|
||||
|
||||
services.AddSingleton<SystemInfoService, SystemInfoService>();
|
||||
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
|
||||
|
||||
var mareConfig = Configuration.GetRequiredSection("MareSynchronos");
|
||||
|
||||
var defaultMethodConfig = new MethodConfig
|
||||
{
|
||||
Names = { MethodName.Default },
|
||||
RetryPolicy = new RetryPolicy
|
||||
{
|
||||
MaxAttempts = 1000,
|
||||
InitialBackoff = TimeSpan.FromSeconds(1),
|
||||
MaxBackoff = TimeSpan.FromSeconds(5),
|
||||
BackoffMultiplier = 1.5,
|
||||
RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable }
|
||||
}
|
||||
};
|
||||
// configure metrics
|
||||
ConfigureMetrics(services);
|
||||
|
||||
var noRetryConfig = new MethodConfig
|
||||
{
|
||||
Names = { MethodName.Default },
|
||||
RetryPolicy = null
|
||||
};
|
||||
// configure file service grpc connection
|
||||
ConfigureFileServiceGrpcClient(services, mareConfig);
|
||||
|
||||
services.AddSingleton<MareMetrics>(m => new MareMetrics(m.GetService<ILogger<MareMetrics>>(), new List<string>
|
||||
{
|
||||
MetricsAPI.CounterInitializedConnections,
|
||||
MetricsAPI.CounterUserPushData,
|
||||
MetricsAPI.CounterUserPushDataTo,
|
||||
MetricsAPI.CounterUsersRegisteredDeleted,
|
||||
MetricsAPI.CounterAuthenticationCacheHits,
|
||||
MetricsAPI.CounterAuthenticationFailures,
|
||||
MetricsAPI.CounterAuthenticationRequests,
|
||||
MetricsAPI.CounterAuthenticationSuccesses
|
||||
}, new List<string>
|
||||
{
|
||||
MetricsAPI.GaugeAuthorizedConnections,
|
||||
MetricsAPI.GaugeConnections,
|
||||
MetricsAPI.GaugePairs,
|
||||
MetricsAPI.GaugePairsPaused,
|
||||
MetricsAPI.GaugeAvailableIOWorkerThreads,
|
||||
MetricsAPI.GaugeAvailableWorkerThreads,
|
||||
MetricsAPI.GaugeGroups,
|
||||
MetricsAPI.GaugeGroupPairs,
|
||||
MetricsAPI.GaugeGroupPairsPaused
|
||||
}));
|
||||
// configure database
|
||||
ConfigureDatabase(services, mareConfig);
|
||||
|
||||
services.AddGrpcClient<FileService.FileServiceClient>(c =>
|
||||
// configure authentication and authorization
|
||||
ConfigureAuthorization(services);
|
||||
|
||||
// configure rate limiting
|
||||
ConfigureIpRateLimiting(services);
|
||||
|
||||
// configure SignalR
|
||||
ConfigureSignalR(services, mareConfig);
|
||||
|
||||
// configure mare specific services
|
||||
ConfigureMareServices(services, mareConfig);
|
||||
}
|
||||
|
||||
private static void ConfigureMareServices(IServiceCollection services, IConfigurationSection mareConfig)
|
||||
{
|
||||
bool isMainServer = mareConfig.GetValue<Uri>(nameof(ServerConfiguration.MainServerGrpcAddress), defaultValue: null) == null;
|
||||
|
||||
services.Configure<ServerConfiguration>(mareConfig);
|
||||
services.Configure<MareConfigurationBase>(mareConfig);
|
||||
services.Configure<MareConfigurationAuthBase>(mareConfig);
|
||||
|
||||
services.AddSingleton<SystemInfoService>();
|
||||
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
|
||||
services.AddHostedService(provider => provider.GetService<SystemInfoService>());
|
||||
// configure services based on main server status
|
||||
ConfigureIdentityServices(services, mareConfig, isMainServer);
|
||||
|
||||
if (isMainServer)
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>(nameof(ServerConfiguration.StaticFileServiceAddress)));
|
||||
}).ConfigureChannel(c =>
|
||||
services.AddSingleton<UserCleanupService>();
|
||||
services.AddHostedService(provider => provider.GetService<UserCleanupService>());
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureSignalR(IServiceCollection services, IConfigurationSection mareConfig)
|
||||
{
|
||||
var signalRServiceBuilder = services.AddSignalR(hubOptions =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
|
||||
});
|
||||
services.AddGrpcClient<IdentificationService.IdentificationServiceClient>(c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>(nameof(ServerConfiguration.ServiceAddress)));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { noRetryConfig } };
|
||||
c.HttpHandler = new SocketsHttpHandler()
|
||||
{
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
hubOptions.MaximumReceiveMessageSize = long.MaxValue;
|
||||
hubOptions.EnableDetailedErrors = true;
|
||||
hubOptions.MaximumParallelInvocationsPerClient = 10;
|
||||
hubOptions.StreamBufferCapacity = 200;
|
||||
|
||||
hubOptions.AddFilter<SignalRLimitFilter>();
|
||||
});
|
||||
|
||||
// configure redis for SignalR
|
||||
var redis = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty);
|
||||
if (!string.IsNullOrEmpty(redis))
|
||||
{
|
||||
signalRServiceBuilder.AddStackExchangeRedis(redis, options =>
|
||||
{
|
||||
options.Configuration.ChannelPrefix = "MareSynchronos";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureIpRateLimiting(IServiceCollection services)
|
||||
{
|
||||
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
|
||||
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
|
||||
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
|
||||
services.AddMemoryCache();
|
||||
services.AddInMemoryRateLimiting();
|
||||
}
|
||||
|
||||
private static void ConfigureAuthorization(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<SecretKeyAuthenticatorService>();
|
||||
services.AddSingleton<GrpcClientIdentificationService>();
|
||||
services.AddTransient<IAuthorizationHandler, UserRequirementHandler>();
|
||||
services.AddHostedService(p => p.GetService<GrpcClientIdentificationService>());
|
||||
|
||||
services.AddDbContextPool<MareDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("MareSynchronosShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
|
||||
|
||||
services.AddAuthentication(SecretKeyAuthenticationHandler.AuthScheme)
|
||||
.AddScheme<AuthenticationSchemeOptions, SecretKeyAuthenticationHandler>(SecretKeyAuthenticationHandler.AuthScheme, options => { options.Validate(); });
|
||||
.AddScheme<AuthenticationSchemeOptions, SecretKeyAuthenticationHandler>(SecretKeyAuthenticationHandler.AuthScheme, options => { options.Validate(); });
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
@@ -157,44 +143,127 @@ public class Startup
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator));
|
||||
});
|
||||
});
|
||||
|
||||
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
|
||||
|
||||
var signalRServiceBuilder = services.AddSignalR(hubOptions =>
|
||||
{
|
||||
hubOptions.MaximumReceiveMessageSize = long.MaxValue;
|
||||
hubOptions.EnableDetailedErrors = true;
|
||||
hubOptions.MaximumParallelInvocationsPerClient = 10;
|
||||
hubOptions.StreamBufferCapacity = 200;
|
||||
|
||||
hubOptions.AddFilter<SignalRLimitFilter>();
|
||||
});
|
||||
|
||||
// add redis related options
|
||||
var redis = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty);
|
||||
if (!string.IsNullOrEmpty(redis))
|
||||
{
|
||||
signalRServiceBuilder.AddStackExchangeRedis(redis, options =>
|
||||
{
|
||||
options.Configuration.ChannelPrefix = "MareSynchronos";
|
||||
});
|
||||
}
|
||||
|
||||
services.AddHostedService(provider => provider.GetService<SystemInfoService>());
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
private void ConfigureDatabase(IServiceCollection services, IConfigurationSection mareConfig)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
services.AddDbContextPool<MareDbContext>(options =>
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseMigrationsEndPoint();
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("MareSynchronosShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
|
||||
}
|
||||
|
||||
private static void ConfigureMetrics(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<MareMetrics>(m => new MareMetrics(m.GetService<ILogger<MareMetrics>>(), new List<string>
|
||||
{
|
||||
MetricsAPI.CounterInitializedConnections,
|
||||
MetricsAPI.CounterUserPushData,
|
||||
MetricsAPI.CounterUserPushDataTo,
|
||||
MetricsAPI.CounterUsersRegisteredDeleted,
|
||||
MetricsAPI.CounterAuthenticationCacheHits,
|
||||
MetricsAPI.CounterAuthenticationFailures,
|
||||
MetricsAPI.CounterAuthenticationRequests,
|
||||
MetricsAPI.CounterAuthenticationSuccesses,
|
||||
}, new List<string>
|
||||
{
|
||||
MetricsAPI.GaugeAuthorizedConnections,
|
||||
MetricsAPI.GaugeConnections,
|
||||
MetricsAPI.GaugePairs,
|
||||
MetricsAPI.GaugePairsPaused,
|
||||
MetricsAPI.GaugeAvailableIOWorkerThreads,
|
||||
MetricsAPI.GaugeAvailableWorkerThreads,
|
||||
MetricsAPI.GaugeGroups,
|
||||
MetricsAPI.GaugeGroupPairs,
|
||||
MetricsAPI.GaugeGroupPairsPaused,
|
||||
MetricsAPI.GaugeUsersRegistered
|
||||
}));
|
||||
}
|
||||
|
||||
private static void ConfigureIdentityServices(IServiceCollection services, IConfigurationSection mareConfig, bool isMainServer)
|
||||
{
|
||||
if (!isMainServer)
|
||||
{
|
||||
var noRetryConfig = new MethodConfig
|
||||
{
|
||||
Names = { MethodName.Default },
|
||||
RetryPolicy = null
|
||||
};
|
||||
|
||||
services.AddGrpcClient<IdentificationService.IdentificationServiceClient>(c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>(nameof(ServerConfiguration.MainServerGrpcAddress)));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { noRetryConfig } };
|
||||
c.HttpHandler = new SocketsHttpHandler()
|
||||
{
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
});
|
||||
|
||||
services.AddGrpcClient<ConfigurationService.ConfigurationServiceClient>(c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>(nameof(ServerConfiguration.MainServerGrpcAddress)));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { noRetryConfig } };
|
||||
c.HttpHandler = new SocketsHttpHandler()
|
||||
{
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
});
|
||||
|
||||
services.AddSingleton<IClientIdentificationService, GrpcClientIdentificationService>();
|
||||
services.AddHostedService(p => p.GetService<IClientIdentificationService>());
|
||||
services.AddSingleton<IConfigurationService<ServerConfiguration>, MareConfigurationServiceClient<ServerConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<MareConfigurationAuthBase>, MareConfigurationServiceClient<MareConfigurationAuthBase>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
app.UseHsts();
|
||||
services.AddSingleton<IdentityHandler>();
|
||||
services.AddSingleton<IClientIdentificationService, LocalClientIdentificationService>();
|
||||
services.AddSingleton<IConfigurationService<ServerConfiguration>, MareConfigurationServiceServer<ServerConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<MareConfigurationAuthBase>, MareConfigurationServiceServer<MareConfigurationAuthBase>>();
|
||||
|
||||
services.AddGrpc();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureFileServiceGrpcClient(IServiceCollection services, IConfigurationSection mareConfig)
|
||||
{
|
||||
var defaultMethodConfig = new MethodConfig
|
||||
{
|
||||
Names = { MethodName.Default },
|
||||
RetryPolicy = new RetryPolicy
|
||||
{
|
||||
MaxAttempts = 1000,
|
||||
InitialBackoff = TimeSpan.FromSeconds(1),
|
||||
MaxBackoff = TimeSpan.FromSeconds(5),
|
||||
BackoffMultiplier = 1.5,
|
||||
RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable }
|
||||
}
|
||||
};
|
||||
services.AddGrpcClient<FileService.FileServiceClient>((serviceProvider, c) =>
|
||||
{
|
||||
c.Address = serviceProvider.GetRequiredService<IConfigurationService<ServerConfiguration>>()
|
||||
.GetValue<Uri>(nameof(ServerConfiguration.StaticFileServiceAddress));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
|
||||
{
|
||||
logger.LogInformation("Running Configure");
|
||||
|
||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationAuthBase>>();
|
||||
|
||||
app.UseIpRateLimiting();
|
||||
|
||||
@@ -202,7 +271,7 @@ public class Startup
|
||||
|
||||
app.UseWebSockets();
|
||||
|
||||
var metricServer = new KestrelMetricServer(4980);
|
||||
var metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(MareConfigurationBase.MetricsPort), 4981));
|
||||
metricServer.Start();
|
||||
|
||||
app.UseAuthentication();
|
||||
@@ -216,6 +285,12 @@ public class Startup
|
||||
options.TransportMaxBufferSize = 5242880;
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
|
||||
});
|
||||
|
||||
if (config.IsMain)
|
||||
{
|
||||
endpoints.MapGrpcService<GrpcIdentityService>().AllowAnonymous();
|
||||
endpoints.MapGrpcService<GrpcConfigurationService<ServerConfiguration>>().AllowAnonymous();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
@@ -8,6 +7,6 @@ public class IdBasedUserIdProvider : IUserIdProvider
|
||||
{
|
||||
public string GetUserId(HubConnectionContext context)
|
||||
{
|
||||
return context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, System.StringComparison.Ordinal))?.Value;
|
||||
return context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using MareSynchronosServer.Hubs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
@@ -14,6 +13,7 @@ public class MareHubLogger
|
||||
_hub = hub;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static object[] Args(params object[] args)
|
||||
{
|
||||
return args;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public record PausedEntry
|
||||
{
|
||||
|
||||
@@ -1,41 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord;
|
||||
using Discord.Interactions;
|
||||
using Discord.Rest;
|
||||
using Discord.WebSocket;
|
||||
using MareSynchronosServices.Identity;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using static MareSynchronosShared.Protos.IdentificationService;
|
||||
|
||||
namespace MareSynchronosServices.Discord;
|
||||
|
||||
internal class DiscordBot : IHostedService
|
||||
{
|
||||
private readonly DiscordBotServices _botServices;
|
||||
private readonly IdentityHandler _identityHandler;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ServicesConfiguration _configuration;
|
||||
private readonly IConfigurationService<ServicesConfiguration> _configurationService;
|
||||
private readonly ILogger<DiscordBot> _logger;
|
||||
private readonly IdentificationServiceClient _identificationServiceClient;
|
||||
private readonly DiscordSocketClient _discordClient;
|
||||
private CancellationTokenSource? _updateStatusCts;
|
||||
private CancellationTokenSource? _vanityUpdateCts;
|
||||
|
||||
public DiscordBot(DiscordBotServices botServices, IdentityHandler identityHandler, IServiceProvider services, IOptions<ServicesConfiguration> configuration, ILogger<DiscordBot> logger)
|
||||
public DiscordBot(DiscordBotServices botServices, IServiceProvider services, IConfigurationService<ServicesConfiguration> configuration,
|
||||
ILogger<DiscordBot> logger, IdentificationServiceClient identificationServiceClient)
|
||||
{
|
||||
_botServices = botServices;
|
||||
_identityHandler = identityHandler;
|
||||
_services = services;
|
||||
_configuration = configuration.Value;
|
||||
_configurationService = configuration;
|
||||
_logger = logger;
|
||||
|
||||
this._identificationServiceClient = identificationServiceClient;
|
||||
_discordClient = new(new DiscordSocketConfig()
|
||||
{
|
||||
DefaultRetryMode = RetryMode.AlwaysRetry
|
||||
@@ -72,7 +65,8 @@ internal class DiscordBot : IHostedService
|
||||
_vanityUpdateCts = new();
|
||||
var guild = (await _discordClient.Rest.GetGuildsAsync()).First();
|
||||
var commands = await guild.GetApplicationCommandsAsync();
|
||||
var vanityCommandId = commands.First(c => c.Name == "setvanityuid").Id;
|
||||
var appId = await _discordClient.GetApplicationInfoAsync().ConfigureAwait(false);
|
||||
var vanityCommandId = commands.First(c => c.ApplicationId == appId.Id && c.Name == "setvanityuid").Id;
|
||||
|
||||
while (!_vanityUpdateCts.IsCancellationRequested)
|
||||
{
|
||||
@@ -176,18 +170,19 @@ internal class DiscordBot : IHostedService
|
||||
_updateStatusCts = new();
|
||||
while (!_updateStatusCts.IsCancellationRequested)
|
||||
{
|
||||
var onlineUsers = _identityHandler.GetOnlineUsers(string.Empty);
|
||||
_logger.LogInformation("Users online: " + onlineUsers);
|
||||
await _discordClient.SetActivityAsync(new Game("Mare for " + onlineUsers + " Users")).ConfigureAwait(false);
|
||||
var onlineUsers = await _identificationServiceClient.GetOnlineUserCountAsync(new MareSynchronosShared.Protos.ServerMessage());
|
||||
_logger.LogInformation("Users online: " + onlineUsers.Count);
|
||||
await _discordClient.SetActivityAsync(new Game("Mare for " + onlineUsers.Count + " Users")).ConfigureAwait(false);
|
||||
await Task.Delay(TimeSpan.FromSeconds(15)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_configuration.DiscordBotToken))
|
||||
var token = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty);
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
await _discordClient.LoginAsync(TokenType.Bot, _configuration.DiscordBotToken).ConfigureAwait(false);
|
||||
await _discordClient.LoginAsync(TokenType.Bot, token).ConfigureAwait(false);
|
||||
await _discordClient.StartAsync().ConfigureAwait(false);
|
||||
|
||||
_discordClient.Ready += DiscordClient_Ready;
|
||||
@@ -199,7 +194,7 @@ internal class DiscordBot : IHostedService
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_configuration.DiscordBotToken))
|
||||
if (!string.IsNullOrEmpty(_configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty)))
|
||||
{
|
||||
await _botServices.Stop();
|
||||
_updateStatusCts?.Cancel();
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosServices.Discord;
|
||||
|
||||
public class DiscordBotServices
|
||||
{
|
||||
public readonly ConcurrentQueue<KeyValuePair<ulong, Action<IServiceProvider>>> verificationQueue = new();
|
||||
public ConcurrentQueue<KeyValuePair<ulong, Action<IServiceProvider>>> VerificationQueue { get; } = new();
|
||||
public ConcurrentDictionary<ulong, DateTime> LastVanityChange = new();
|
||||
public ConcurrentDictionary<string, DateTime> LastVanityGidChange = new();
|
||||
public ConcurrentDictionary<ulong, string> DiscordLodestoneMapping = new();
|
||||
@@ -19,29 +13,27 @@ public class DiscordBotServices
|
||||
public readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" };
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ServicesConfiguration Configuration { get; init; }
|
||||
public ILogger<DiscordBotServices> Logger { get; init; }
|
||||
public MareMetrics Metrics { get; init; }
|
||||
public Random Random { get; init; }
|
||||
private CancellationTokenSource? verificationTaskCts;
|
||||
|
||||
public DiscordBotServices(IOptions<ServicesConfiguration> configuration, IServiceProvider serviceProvider, ILogger<DiscordBotServices> logger, MareMetrics metrics)
|
||||
public DiscordBotServices(IServiceProvider serviceProvider, ILogger<DiscordBotServices> logger, MareMetrics metrics)
|
||||
{
|
||||
Configuration = configuration.Value;
|
||||
_serviceProvider = serviceProvider;
|
||||
Logger = logger;
|
||||
Metrics = metrics;
|
||||
Random = new();
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
public Task Start()
|
||||
{
|
||||
_ = ProcessVerificationQueue();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Stop()
|
||||
public Task Stop()
|
||||
{
|
||||
verificationTaskCts?.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task ProcessVerificationQueue()
|
||||
@@ -49,7 +41,7 @@ public class DiscordBotServices
|
||||
verificationTaskCts = new CancellationTokenSource();
|
||||
while (!verificationTaskCts.IsCancellationRequested)
|
||||
{
|
||||
if (verificationQueue.TryDequeue(out var queueitem))
|
||||
if (VerificationQueue.TryDequeue(out var queueitem))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -61,8 +53,8 @@ public class DiscordBotServices
|
||||
{
|
||||
Logger.LogError(e, "Error during queue work");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,15 @@
|
||||
using Discord.Interactions;
|
||||
using MareSynchronosShared.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Discord.WebSocket;
|
||||
using System.Linq;
|
||||
using Prometheus;
|
||||
using MareSynchronosShared.Models;
|
||||
using MareSynchronosServices.Identity;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using System.Net.Http;
|
||||
using MareSynchronosShared.Utils;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronosShared.Services;
|
||||
using static MareSynchronosShared.Protos.IdentificationService;
|
||||
using static System.Formats.Asn1.AsnWriter;
|
||||
|
||||
namespace MareSynchronosServices.Discord;
|
||||
|
||||
@@ -30,22 +25,30 @@ public class LodestoneModal : IModal
|
||||
|
||||
public class MareModule : InteractionModuleBase
|
||||
{
|
||||
private readonly ILogger<MareModule> _logger;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly DiscordBotServices _botServices;
|
||||
private readonly IdentityHandler _identityHandler;
|
||||
private readonly CleanupService _cleanupService;
|
||||
private readonly IdentificationServiceClient _identificationServiceClient;
|
||||
private readonly IConfigurationService<ServerConfiguration> _mareClientConfigurationService;
|
||||
private Random random = new();
|
||||
|
||||
public MareModule(IServiceProvider services, DiscordBotServices botServices, IdentityHandler identityHandler, CleanupService cleanupService)
|
||||
public MareModule(ILogger<MareModule> logger, IServiceProvider services, DiscordBotServices botServices,
|
||||
IdentificationServiceClient identificationServiceClient, IConfigurationService<ServerConfiguration> mareClientConfigurationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_services = services;
|
||||
_botServices = botServices;
|
||||
_identityHandler = identityHandler;
|
||||
_cleanupService = cleanupService;
|
||||
_identificationServiceClient = identificationServiceClient;
|
||||
_mareClientConfigurationService = mareClientConfigurationService;
|
||||
}
|
||||
|
||||
[SlashCommand("register", "Starts the registration process for the Mare Synchronos server of this Discord")]
|
||||
public async Task Register([Summary("overwrite", "Overwrites your old account")] bool overwrite = false)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}:{params}",
|
||||
Context.Client.CurrentUser.Id, nameof(Register),
|
||||
string.Join(",", new[] { $"{nameof(overwrite)}:{overwrite}" }));
|
||||
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
if (overwrite)
|
||||
@@ -60,6 +63,10 @@ public class MareModule : InteractionModuleBase
|
||||
[SlashCommand("setvanityuid", "Sets your Vanity UID.")]
|
||||
public async Task SetVanityUid([Summary("vanity_uid", "Desired Vanity UID")] string vanityUid)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}:{params}",
|
||||
Context.Client.CurrentUser.Id, nameof(SetVanityUid),
|
||||
string.Join(",", new[] { $"{nameof(vanityUid)}:{vanityUid}" }));
|
||||
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
@@ -75,6 +82,10 @@ public class MareModule : InteractionModuleBase
|
||||
[Summary("syncshell_id", "Syncshell ID")] string syncshellId,
|
||||
[Summary("vanity_syncshell_id", "Desired Vanity Syncshell ID")] string vanityId)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}:{params}",
|
||||
Context.Client.CurrentUser.Id, nameof(SetSyncshellVanityId),
|
||||
string.Join(",", new[] { $"{nameof(syncshellId)}:{syncshellId}", $"{nameof(vanityId)}:{vanityId}" }));
|
||||
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
@@ -88,10 +99,12 @@ public class MareModule : InteractionModuleBase
|
||||
[SlashCommand("verify", "Finishes the registration process for the Mare Synchronos server of this Discord")]
|
||||
public async Task Verify()
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}",
|
||||
Context.Client.CurrentUser.Id, nameof(Verify));
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
if (_botServices.verificationQueue.Any(u => u.Key == Context.User.Id))
|
||||
if (_botServices.VerificationQueue.Any(u => u.Key == Context.User.Id))
|
||||
{
|
||||
eb.WithTitle("Already queued for verfication");
|
||||
eb.WithDescription("You are already queued for verification. Please wait.");
|
||||
@@ -106,7 +119,7 @@ public class MareModule : InteractionModuleBase
|
||||
else
|
||||
{
|
||||
await DeferAsync(ephemeral: true).ConfigureAwait(false);
|
||||
_botServices.verificationQueue.Enqueue(new KeyValuePair<ulong, Action<IServiceProvider>>(Context.User.Id, async (sp) => await HandleVerifyAsync((SocketSlashCommand)Context.Interaction, sp)));
|
||||
_botServices.VerificationQueue.Enqueue(new KeyValuePair<ulong, Action<IServiceProvider>>(Context.User.Id, async (sp) => await HandleVerifyAsync((SocketSlashCommand)Context.Interaction, sp)));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -114,10 +127,12 @@ public class MareModule : InteractionModuleBase
|
||||
[SlashCommand("verify_relink", "Finishes the relink process for your user on the Mare Synchronos server of this Discord")]
|
||||
public async Task VerifyRelink()
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}",
|
||||
Context.Client.CurrentUser.Id, nameof(VerifyRelink));
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
if (_botServices.verificationQueue.Any(u => u.Key == Context.User.Id))
|
||||
if (_botServices.VerificationQueue.Any(u => u.Key == Context.User.Id))
|
||||
{
|
||||
eb.WithTitle("Already queued for verfication");
|
||||
eb.WithDescription("You are already queued for verification. Please wait.");
|
||||
@@ -132,7 +147,7 @@ public class MareModule : InteractionModuleBase
|
||||
else
|
||||
{
|
||||
await DeferAsync(ephemeral: true).ConfigureAwait(false);
|
||||
_botServices.verificationQueue.Enqueue(new KeyValuePair<ulong, Action<IServiceProvider>>(Context.User.Id, async (sp) => await HandleVerifyRelinkAsync((SocketSlashCommand)Context.Interaction, sp)));
|
||||
_botServices.VerificationQueue.Enqueue(new KeyValuePair<ulong, Action<IServiceProvider>>(Context.User.Id, async (sp) => await HandleVerifyRelinkAsync((SocketSlashCommand)Context.Interaction, sp)));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -140,6 +155,8 @@ public class MareModule : InteractionModuleBase
|
||||
[SlashCommand("recover", "Allows you to recover your account by generating a new secret key")]
|
||||
public async Task Recover()
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}",
|
||||
Context.Client.CurrentUser.Id, nameof(Recover));
|
||||
await RespondWithModalAsync<LodestoneModal>("recover_modal").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -148,6 +165,9 @@ public class MareModule : InteractionModuleBase
|
||||
[Summary("discord_user", "ADMIN ONLY: Discord User to check for")] IUser? discordUser = null,
|
||||
[Summary("uid", "ADMIN ONLY: UID to check for")] string? uid = null)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}",
|
||||
Context.Client.CurrentUser.Id, nameof(UserInfo));
|
||||
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
EmbedBuilder eb = new();
|
||||
@@ -161,12 +181,32 @@ public class MareModule : InteractionModuleBase
|
||||
[SlashCommand("relink", "Allows you to link a new Discord account to an existing Mare account")]
|
||||
public async Task Relink()
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}",
|
||||
Context.Client.CurrentUser.Id, nameof(Relink));
|
||||
await RespondWithModalAsync<LodestoneModal>("relink_modal").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[SlashCommand("useradd", "ADMIN ONLY: add a user unconditionally to the Database")]
|
||||
public async Task UserAdd([Summary("desired_uid", "Desired UID")] string desiredUid)
|
||||
{
|
||||
_logger.LogInformation("SlashCommand:{userId}:{Method}:{params}",
|
||||
Context.Client.CurrentUser.Id, nameof(UserAdd),
|
||||
string.Join(",", new[] { $"{nameof(desiredUid)}:{desiredUid}" }));
|
||||
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
var embed = await HandleUserAdd(desiredUid, Context.User.Id);
|
||||
|
||||
await RespondAsync(embeds: new[] { embed }, ephemeral: true).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
[ModalInteraction("recover_modal")]
|
||||
public async Task RecoverModal(LodestoneModal modal)
|
||||
{
|
||||
_logger.LogInformation("Modal:{userId}:{Method}",
|
||||
Context.Client.CurrentUser.Id, nameof(RecoverModal));
|
||||
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
var embed = await HandleRecoverModalAsync(modal, Context.User.Id).ConfigureAwait(false);
|
||||
@@ -177,6 +217,9 @@ public class MareModule : InteractionModuleBase
|
||||
[ModalInteraction("register_modal")]
|
||||
public async Task RegisterModal(LodestoneModal modal)
|
||||
{
|
||||
_logger.LogInformation("Modal:{userId}:{Method}",
|
||||
Context.Client.CurrentUser.Id, nameof(RegisterModal));
|
||||
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
var embed = await HandleRegisterModalAsync(modal, Context.User.Id).ConfigureAwait(false);
|
||||
@@ -187,6 +230,9 @@ public class MareModule : InteractionModuleBase
|
||||
[ModalInteraction("relink_modal")]
|
||||
public async Task RelinkModal(LodestoneModal modal)
|
||||
{
|
||||
_logger.LogInformation("Modal:{userId}:{Method}",
|
||||
Context.Client.CurrentUser.Id, nameof(RelinkModal));
|
||||
|
||||
await TryRespondAsync(async () =>
|
||||
{
|
||||
var embed = await HandleRelinkModalAsync(modal, Context.User.Id).ConfigureAwait(false);
|
||||
@@ -194,6 +240,51 @@ public class MareModule : InteractionModuleBase
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<Embed> HandleUserAdd(string desiredUid, ulong discordUserId)
|
||||
{
|
||||
var embed = new EmbedBuilder();
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
using var db = scope.ServiceProvider.GetService<MareDbContext>();
|
||||
if (!(await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(a => a.DiscordId == discordUserId))?.User?.IsAdmin ?? true)
|
||||
{
|
||||
embed.WithTitle("Failed to add user");
|
||||
embed.WithDescription("No permission");
|
||||
}
|
||||
else if (db.Users.Any(u => u.UID == desiredUid || u.Alias == desiredUid))
|
||||
{
|
||||
embed.WithTitle("Failed to add user");
|
||||
embed.WithDescription("Already in Database");
|
||||
}
|
||||
else
|
||||
{
|
||||
User newUser = new()
|
||||
{
|
||||
IsAdmin = false,
|
||||
IsModerator = false,
|
||||
LastLoggedIn = DateTime.UtcNow,
|
||||
UID = desiredUid,
|
||||
};
|
||||
|
||||
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
var auth = new Auth()
|
||||
{
|
||||
HashedKey = StringUtils.Sha256String(computedHash),
|
||||
User = newUser,
|
||||
};
|
||||
|
||||
await db.Users.AddAsync(newUser);
|
||||
await db.Auth.AddAsync(auth);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
embed.WithTitle("Successfully added " + desiredUid);
|
||||
embed.WithDescription("Secret Key: " + computedHash);
|
||||
}
|
||||
|
||||
return embed.Build();
|
||||
}
|
||||
|
||||
private async Task TryRespondAsync(Action act)
|
||||
{
|
||||
try
|
||||
@@ -258,7 +349,7 @@ public class MareModule : InteractionModuleBase
|
||||
var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == userToCheckForDiscordId).ConfigureAwait(false);
|
||||
var dbUser = lodestoneUser.User;
|
||||
var auth = await db.Auth.SingleOrDefaultAsync(u => u.UserUID == dbUser.UID).ConfigureAwait(false);
|
||||
var identity = await _identityHandler.GetIdentForuid(dbUser.UID).ConfigureAwait(false);
|
||||
var identity = await _identificationServiceClient.GetIdentForUidAsync(new MareSynchronosShared.Protos.UidMessage { Uid = dbUser.UID });
|
||||
var groups = await db.Groups.Where(g => g.OwnerUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
var groupsJoined = await db.GroupPairs.Where(g => g.GroupUserUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -271,7 +362,7 @@ public class MareModule : InteractionModuleBase
|
||||
eb.AddField("Vanity UID", dbUser.Alias);
|
||||
}
|
||||
eb.AddField("Last Online (UTC)", dbUser.LastLoggedIn.ToString("U"));
|
||||
eb.AddField("Currently online: ", !string.IsNullOrEmpty(identity.CharacterIdent));
|
||||
eb.AddField("Currently online: ", !string.IsNullOrEmpty(identity.Ident));
|
||||
eb.AddField("Hashed Secret Key", auth.HashedKey);
|
||||
eb.AddField("Joined Syncshells", groupsJoined.Count);
|
||||
eb.AddField("Owned Syncshells", groups.Count);
|
||||
@@ -285,9 +376,9 @@ public class MareModule : InteractionModuleBase
|
||||
eb.AddField("Owned Syncshell " + group.GID + " User Count", syncShellUserCount);
|
||||
}
|
||||
|
||||
if (isAdminCall && !string.IsNullOrEmpty(identity.CharacterIdent))
|
||||
if (isAdminCall && !string.IsNullOrEmpty(identity.Ident))
|
||||
{
|
||||
eb.AddField("Character Ident", identity.CharacterIdent);
|
||||
eb.AddField("Character Ident", identity.Ident);
|
||||
}
|
||||
|
||||
return eb;
|
||||
@@ -635,7 +726,9 @@ public class MareModule : InteractionModuleBase
|
||||
{
|
||||
if (discordAuthedUser.User != null)
|
||||
{
|
||||
await _cleanupService.PurgeUser(discordAuthedUser.User, db);
|
||||
var maxGroupsByUser = _mareClientConfigurationService.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3);
|
||||
|
||||
await SharedDbFunctions.PurgeUser(_logger, discordAuthedUser.User, db, maxGroupsByUser);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -657,7 +750,7 @@ public class MareModule : InteractionModuleBase
|
||||
var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == cmd.User.Id);
|
||||
if (lodestoneAuth != null && _botServices.DiscordRelinkLodestoneMapping.ContainsKey(cmd.User.Id))
|
||||
{
|
||||
var randomServer = _botServices.LodestoneServers[_botServices.Random.Next(_botServices.LodestoneServers.Length)];
|
||||
var randomServer = _botServices.LodestoneServers[random.Next(_botServices.LodestoneServers.Length)];
|
||||
var response = await req.GetAsync($"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{_botServices.DiscordRelinkLodestoneMapping[cmd.User.Id]}").ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -731,7 +824,7 @@ public class MareModule : InteractionModuleBase
|
||||
var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == cmd.User.Id);
|
||||
if (lodestoneAuth != null && _botServices.DiscordLodestoneMapping.ContainsKey(cmd.User.Id))
|
||||
{
|
||||
var randomServer = _botServices.LodestoneServers[_botServices.Random.Next(_botServices.LodestoneServers.Length)];
|
||||
var randomServer = _botServices.LodestoneServers[random.Next(_botServices.LodestoneServers.Length)];
|
||||
var response = await req.GetAsync($"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{_botServices.DiscordLodestoneMapping[cmd.User.Id]}").ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -757,11 +850,7 @@ public class MareModule : InteractionModuleBase
|
||||
user.IsAdmin = true;
|
||||
}
|
||||
|
||||
if (_botServices.Configuration.PurgeUnusedAccounts)
|
||||
{
|
||||
var purgedDays = _botServices.Configuration.PurgeUnusedAccountsPeriodInDays;
|
||||
user.LastLoggedIn = DateTime.UtcNow - TimeSpan.FromDays(purgedDays) + TimeSpan.FromDays(1);
|
||||
}
|
||||
user.LastLoggedIn = DateTime.UtcNow;
|
||||
|
||||
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||
var auth = new Auth()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
using MareSynchronosServices;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
|
||||
public class Program
|
||||
{
|
||||
@@ -24,10 +19,13 @@ public class Program
|
||||
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, dbContext.Users.Count());
|
||||
|
||||
var options = host.Services.GetService<IOptions<ServicesConfiguration>>();
|
||||
var options = host.Services.GetService<IConfigurationService<ServicesConfiguration>>();
|
||||
var optionsServer = host.Services.GetService<IConfigurationService<ServerConfiguration>>();
|
||||
var logger = host.Services.GetService<ILogger<Program>>();
|
||||
logger.LogInformation("Loaded MareSynchronos Services Configuration");
|
||||
logger.LogInformation(options.Value.ToString());
|
||||
logger.LogInformation("Loaded MareSynchronos Services Configuration (IsMain: {isMain})", options.IsMain);
|
||||
logger.LogInformation(options.ToString());
|
||||
logger.LogInformation("Loaded MareSynchronos Server Configuration (IsMain: {isMain})", optionsServer.IsMain);
|
||||
logger.LogInformation(optionsServer.ToString());
|
||||
}
|
||||
|
||||
host.Run();
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using MareSynchronosShared.Utils;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronosServices;
|
||||
|
||||
public class ServicesConfiguration : MareConfigurationBase
|
||||
{
|
||||
public string DiscordBotToken { get; set; } = string.Empty;
|
||||
public bool PurgeUnusedAccounts { get; set; } = false;
|
||||
public int PurgeUnusedAccountsPeriodInDays { get; set; } = 14;
|
||||
public int MaxExistingGroupsByUser { get; set; } = 3;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine(base.ToString());
|
||||
sb.AppendLine($"{nameof(DiscordBotToken)} => {DiscordBotToken}");
|
||||
sb.AppendLine($"{nameof(PurgeUnusedAccounts)} => {PurgeUnusedAccounts}");
|
||||
sb.AppendLine($"{nameof(PurgeUnusedAccountsPeriodInDays)} => {PurgeUnusedAccountsPeriodInDays}");
|
||||
sb.AppendLine($"{nameof(MaxExistingGroupsByUser)} => {MaxExistingGroupsByUser}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
using MareSynchronosServices.Discord;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Prometheus;
|
||||
using System.Collections.Generic;
|
||||
using MareSynchronosServices.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Grpc.Net.Client.Configuration;
|
||||
using MareSynchronosShared.Protos;
|
||||
using MareSynchronosShared.Services;
|
||||
|
||||
namespace MareSynchronosServices;
|
||||
|
||||
@@ -25,6 +21,8 @@ public class Startup
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
var mareConfig = Configuration.GetSection("MareSynchronos");
|
||||
|
||||
services.AddDbContextPool<MareDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
@@ -34,32 +32,58 @@ public class Startup
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, Configuration.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
|
||||
|
||||
services.AddSingleton(m => new MareMetrics(m.GetService<ILogger<MareMetrics>>(), new List<string> {
|
||||
}, new List<string>
|
||||
services.AddSingleton(m => new MareMetrics(m.GetService<ILogger<MareMetrics>>(), new List<string> { },
|
||||
new List<string>
|
||||
{
|
||||
MetricsAPI.GaugeUsersRegistered
|
||||
}));
|
||||
|
||||
var noRetryConfig = new MethodConfig
|
||||
{
|
||||
Names = { MethodName.Default },
|
||||
RetryPolicy = null
|
||||
};
|
||||
|
||||
services.AddGrpcClient<IdentificationService.IdentificationServiceClient>(c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>(nameof(ServicesConfiguration.MainServerGrpcAddress)));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { noRetryConfig } };
|
||||
c.HttpHandler = new SocketsHttpHandler()
|
||||
{
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
});
|
||||
|
||||
services.AddGrpcClient<ConfigurationService.ConfigurationServiceClient>(c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>(nameof(ServicesConfiguration.MainServerGrpcAddress)));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { noRetryConfig } };
|
||||
c.HttpHandler = new SocketsHttpHandler()
|
||||
{
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
});
|
||||
|
||||
services.Configure<ServicesConfiguration>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
services.Configure<ServerConfiguration>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
services.Configure<MareConfigurationAuthBase>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
services.AddSingleton(Configuration);
|
||||
services.AddSingleton<DiscordBotServices>();
|
||||
services.AddSingleton<IdentityHandler>();
|
||||
services.AddSingleton<CleanupService>();
|
||||
services.AddHostedService(provider => provider.GetService<CleanupService>());
|
||||
services.AddHostedService<DiscordBot>();
|
||||
services.AddGrpc();
|
||||
services.AddSingleton<IConfigurationService<ServicesConfiguration>, MareConfigurationServiceServer<ServicesConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<ServerConfiguration>, MareConfigurationServiceClient<ServerConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<MareConfigurationAuthBase>, MareConfigurationServiceClient<MareConfigurationAuthBase>>();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
app.UseRouting();
|
||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationAuthBase>>();
|
||||
|
||||
var metricServer = new KestrelMetricServer(4982);
|
||||
var metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(MareConfigurationBase.MetricsPort), 4982));
|
||||
metricServer.Start();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapGrpcService<IdentityService>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using MareSynchronosServer;
|
||||
using MareSynchronosShared.Data;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -13,7 +12,6 @@ public class SecretKeyAuthenticationHandler : AuthenticationHandler<Authenticati
|
||||
{
|
||||
public const string AuthScheme = "SecretKeyGrpcAuth";
|
||||
|
||||
private readonly MareDbContext _mareDbContext;
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
private readonly SecretKeyAuthenticatorService secretKeyAuthenticatorService;
|
||||
|
||||
@@ -26,6 +24,12 @@ public class SecretKeyAuthenticationHandler : AuthenticationHandler<Authenticati
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var endpoint = Context.GetEndpoint();
|
||||
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
|
||||
{
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
if (!Request.Headers.TryGetValue("Authorization", out var authHeader))
|
||||
{
|
||||
authHeader = string.Empty;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -13,25 +14,15 @@ public class SecretKeyAuthenticatorService
|
||||
{
|
||||
private readonly MareMetrics _metrics;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IConfigurationService<MareConfigurationAuthBase> _configurationService;
|
||||
private readonly ILogger<SecretKeyAuthenticatorService> _logger;
|
||||
private readonly ConcurrentDictionary<string, SecretKeyAuthReply> _cachedPositiveResponses = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, SecretKeyFailedAuthorization?> _failedAuthorizations = new(StringComparer.Ordinal);
|
||||
private readonly int _failedAttemptsForTempBan;
|
||||
private readonly int _tempBanMinutes;
|
||||
private readonly List<string> _whitelistedIps;
|
||||
|
||||
public SecretKeyAuthenticatorService(MareMetrics metrics, IServiceScopeFactory serviceScopeFactory, IOptions<MareConfigurationAuthBase> configuration, ILogger<SecretKeyAuthenticatorService> logger)
|
||||
public SecretKeyAuthenticatorService(MareMetrics metrics, IServiceScopeFactory serviceScopeFactory, IConfigurationService<MareConfigurationAuthBase> configuration, ILogger<SecretKeyAuthenticatorService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
var config = configuration.Value;
|
||||
_failedAttemptsForTempBan = config.FailedAuthForTempBan;
|
||||
_tempBanMinutes = config.TempBanDurationInMinutes;
|
||||
_whitelistedIps = config.WhitelistedIps;
|
||||
foreach (var ip in _whitelistedIps)
|
||||
{
|
||||
logger.LogInformation("Whitelisted IP: " + ip);
|
||||
}
|
||||
|
||||
_configurationService = configuration;
|
||||
_metrics = metrics;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
@@ -46,7 +37,8 @@ public class SecretKeyAuthenticatorService
|
||||
return cachedPositiveResponse;
|
||||
}
|
||||
|
||||
if (_failedAuthorizations.TryGetValue(ip, out var existingFailedAuthorization) && existingFailedAuthorization.FailedAttempts > _failedAttemptsForTempBan)
|
||||
if (_failedAuthorizations.TryGetValue(ip, out var existingFailedAuthorization)
|
||||
&& existingFailedAuthorization.FailedAttempts > _configurationService.GetValueOrDefault(nameof(MareConfigurationAuthBase.FailedAuthForTempBan), 5))
|
||||
{
|
||||
if (existingFailedAuthorization.ResetTask == null)
|
||||
{
|
||||
@@ -54,7 +46,7 @@ public class SecretKeyAuthenticatorService
|
||||
|
||||
existingFailedAuthorization.ResetTask = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(_tempBanMinutes)).ConfigureAwait(false);
|
||||
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(MareConfigurationAuthBase.TempBanDurationInMinutes), 5))).ConfigureAwait(false);
|
||||
|
||||
}).ContinueWith((t) =>
|
||||
{
|
||||
@@ -96,7 +88,8 @@ public class SecretKeyAuthenticatorService
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures);
|
||||
|
||||
_logger.LogWarning("Failed authorization from {ip}", ip);
|
||||
if (!_whitelistedIps.Any(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase)))
|
||||
var whitelisted = _configurationService.GetValueOrDefault(nameof(MareConfigurationAuthBase.WhitelistedIps), new List<string>());
|
||||
if (!whitelisted.Any(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (_failedAuthorizations.TryGetValue(ip, out var auth))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MareSynchronosServer;
|
||||
namespace MareSynchronosShared;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
|
||||
@@ -20,6 +20,19 @@ service IdentificationService {
|
||||
rpc ReceiveStreamIdentStatusChange (ServerMessage) returns (stream IdentChange);
|
||||
}
|
||||
|
||||
service ConfigurationService {
|
||||
rpc GetConfigurationEntry (KeyMessage) returns (ValueMessage);
|
||||
}
|
||||
|
||||
message KeyMessage {
|
||||
string key = 1;
|
||||
string default = 2;
|
||||
}
|
||||
|
||||
message ValueMessage {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message Empty { }
|
||||
|
||||
message MultiUidMessage {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Grpc.Core;
|
||||
using MareSynchronosShared.Protos;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosShared.Services;
|
||||
|
||||
[Authorize]
|
||||
[AllowAnonymous]
|
||||
public class GrpcConfigurationService<T> : ConfigurationService.ConfigurationServiceBase where T : class, IMareConfiguration
|
||||
{
|
||||
private readonly T _config;
|
||||
private readonly ILogger<GrpcConfigurationService<T>> logger;
|
||||
|
||||
public GrpcConfigurationService(IOptions<T> config, ILogger<GrpcConfigurationService<T>> logger)
|
||||
{
|
||||
_config = config.Value;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
public override Task<ValueMessage> GetConfigurationEntry(KeyMessage request, ServerCallContext context)
|
||||
{
|
||||
logger.LogInformation("Remote requested {key}", request.Key);
|
||||
var returnVal = _config.SerializeValue(request.Key, request.Default);
|
||||
return Task.FromResult(new ValueMessage() { Value = returnVal });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using MareSynchronosShared.Utils;
|
||||
|
||||
namespace MareSynchronosShared.Services;
|
||||
|
||||
public interface IConfigurationService<T> where T : class, IMareConfiguration
|
||||
{
|
||||
bool IsMain { get; }
|
||||
T1 GetValue<T1>(string key);
|
||||
T1 GetValueOrDefault<T1>(string key, T1 defaultValue);
|
||||
string ToString();
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using Grpc.Net.ClientFactory;
|
||||
using MareSynchronosShared.Protos;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using static MareSynchronosShared.Protos.ConfigurationService;
|
||||
|
||||
namespace MareSynchronosShared.Services;
|
||||
|
||||
public class MareConfigurationServiceClient<T> : IConfigurationService<T> where T : class, IMareConfiguration
|
||||
{
|
||||
internal record RemoteCachedEntry(object Value, DateTime Inserted);
|
||||
|
||||
private readonly T _config;
|
||||
private readonly ConcurrentDictionary<string, RemoteCachedEntry> _cachedRemoteProperties = new(StringComparer.Ordinal);
|
||||
private readonly ILogger<MareConfigurationServiceClient<T>> _logger;
|
||||
private readonly ConfigurationServiceClient _configurationServiceClient;
|
||||
|
||||
public MareConfigurationServiceClient(ILogger<MareConfigurationServiceClient<T>> logger, IOptions<T> config, ConfigurationServiceClient configurationServiceClient)
|
||||
{
|
||||
_config = config.Value;
|
||||
_logger = logger;
|
||||
_configurationServiceClient = configurationServiceClient;
|
||||
}
|
||||
|
||||
public MareConfigurationServiceClient(ILogger<MareConfigurationServiceClient<T>> logger, IOptions<T> config, GrpcClientFactory grpcClientFactory, string grpcClientName)
|
||||
{
|
||||
_config = config.Value;
|
||||
_logger = logger;
|
||||
_configurationServiceClient = grpcClientFactory.CreateClient<ConfigurationServiceClient>(grpcClientName);
|
||||
}
|
||||
|
||||
public bool IsMain => false;
|
||||
|
||||
public T1 GetValueOrDefault<T1>(string key, T1 defaultValue)
|
||||
{
|
||||
var prop = _config.GetType().GetProperty(key);
|
||||
if (prop == null) return defaultValue;
|
||||
if (prop.PropertyType != typeof(T1)) throw new InvalidCastException($"Invalid Cast: Property {key} is {prop.PropertyType}, wanted: {typeof(T1)}");
|
||||
bool isRemote = prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), inherit: true).Any();
|
||||
if (isRemote)
|
||||
{
|
||||
bool isCurrent = false;
|
||||
if (_cachedRemoteProperties.TryGetValue(key, out var existingEntry) && existingEntry.Inserted > DateTime.Now - TimeSpan.FromMinutes(30))
|
||||
{
|
||||
isCurrent = true;
|
||||
}
|
||||
|
||||
if (!isCurrent)
|
||||
{
|
||||
var result = GetValueFromGrpc(key, defaultValue, prop.PropertyType);
|
||||
if (result == null) return defaultValue;
|
||||
_cachedRemoteProperties[key] = result;
|
||||
return (T1)_cachedRemoteProperties[key].Value;
|
||||
}
|
||||
}
|
||||
|
||||
var value = prop.GetValue(_config);
|
||||
return (T1)value;
|
||||
}
|
||||
|
||||
private RemoteCachedEntry? GetValueFromGrpc(string key, object defaultValue, Type t)
|
||||
{
|
||||
// grab stuff from grpc
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Getting {key} from Grpc", key);
|
||||
var response = _configurationServiceClient.GetConfigurationEntry(new KeyMessage { Key = key, Default = Convert.ToString(defaultValue, CultureInfo.InvariantCulture) });
|
||||
_logger.LogInformation("Grpc Response for {key} = {value}", key, response.Value);
|
||||
return new RemoteCachedEntry(JsonSerializer.Deserialize(response.Value, t), DateTime.Now);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public T1 GetValue<T1>(string key)
|
||||
{
|
||||
var prop = _config.GetType().GetProperty(key);
|
||||
if (prop == null) throw new KeyNotFoundException(key);
|
||||
if (prop.PropertyType != typeof(T1)) throw new InvalidCastException($"Invalid Cast: Property {key} is {prop.PropertyType}, wanted: {typeof(T1)}");
|
||||
bool isRemote = prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), inherit: true).Any();
|
||||
if (isRemote)
|
||||
{
|
||||
bool isCurrent = false;
|
||||
if (_cachedRemoteProperties.TryGetValue(key, out var existingEntry) && existingEntry.Inserted > DateTime.Now - TimeSpan.FromMinutes(30))
|
||||
{
|
||||
isCurrent = true;
|
||||
}
|
||||
|
||||
if (!isCurrent)
|
||||
{
|
||||
var result = GetValueFromGrpc(key, null, prop.PropertyType);
|
||||
if (result == null) throw new KeyNotFoundException(key);
|
||||
_cachedRemoteProperties[key] = result;
|
||||
}
|
||||
|
||||
if (!_cachedRemoteProperties.ContainsKey(key)) throw new KeyNotFoundException(key);
|
||||
|
||||
return (T1)_cachedRemoteProperties[key].Value;
|
||||
}
|
||||
|
||||
var value = prop.GetValue(_config);
|
||||
return (T1)value;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var props = _config.GetType().GetProperties();
|
||||
StringBuilder sb = new();
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var isRemote = prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), true).Any();
|
||||
var mi = GetType().GetMethod(nameof(GetValue)).MakeGenericMethod(prop.PropertyType);
|
||||
var val = mi.Invoke(this, new[] { prop.Name });
|
||||
var value = isRemote ? val : prop.GetValue(_config);
|
||||
sb.AppendLine($"{prop.Name} (IsRemote: {isRemote}) => {value}");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronosShared.Services;
|
||||
|
||||
public class MareConfigurationServiceServer<T> : IConfigurationService<T> where T : class, IMareConfiguration
|
||||
{
|
||||
private readonly T _config;
|
||||
public bool IsMain => true;
|
||||
|
||||
public MareConfigurationServiceServer(IOptions<T> config)
|
||||
{
|
||||
_config = config.Value;
|
||||
}
|
||||
|
||||
public T1 GetValueOrDefault<T1>(string key, T1 defaultValue)
|
||||
{
|
||||
return _config.GetValueOrDefault<T1>(key, defaultValue);
|
||||
}
|
||||
|
||||
public T1 GetValue<T1>(string key)
|
||||
{
|
||||
return _config.GetValue<T1>(key);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var props = _config.GetType().GetProperties();
|
||||
StringBuilder sb = new();
|
||||
foreach (var prop in props)
|
||||
{
|
||||
sb.AppendLine($"{prop.Name} (IsRemote: {prop.GetCustomAttributes(typeof(RemoteConfigurationAttribute), true).Any()}) => {prop.GetValue(_config)}");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MareSynchronosShared.Utils;
|
||||
|
||||
public interface IMareConfiguration
|
||||
{
|
||||
T GetValueOrDefault<T>(string key, T defaultValue);
|
||||
T GetValue<T>(string key);
|
||||
string SerializeValue(string key, string defaultValue);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronosShared.Utils;
|
||||
|
||||
public class MareConfigurationAuthBase : MareConfigurationBase
|
||||
{
|
||||
public Uri MainServerGrpcAddress { get; set; } = null;
|
||||
[RemoteConfiguration]
|
||||
public int FailedAuthForTempBan { get; set; } = 5;
|
||||
[RemoteConfiguration]
|
||||
public int TempBanDurationInMinutes { get; set; } = 5;
|
||||
[RemoteConfiguration]
|
||||
public List<string> WhitelistedIps { get; set; } = new();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine(base.ToString());
|
||||
sb.AppendLine($"{nameof(MainServerGrpcAddress)} => {MainServerGrpcAddress}");
|
||||
sb.AppendLine($"{nameof(FailedAuthForTempBan)} => {FailedAuthForTempBan}");
|
||||
sb.AppendLine($"{nameof(TempBanDurationInMinutes)} => {TempBanDurationInMinutes}");
|
||||
sb.AppendLine($"{nameof(WhitelistedIps)} => {string.Join(", ", WhitelistedIps)}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,46 @@
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MareSynchronosShared.Utils;
|
||||
|
||||
public class MareConfigurationBase
|
||||
public class MareConfigurationBase : IMareConfiguration
|
||||
{
|
||||
public int DbContextPoolSize { get; set; } = 100;
|
||||
public string ShardName { get; set; } = string.Empty;
|
||||
public int MetricsPort { get; set; } = 4981;
|
||||
|
||||
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(ShardName)} => {ShardName}");
|
||||
sb.AppendLine($"{nameof(DbContextPoolSize)} => {DbContextPoolSize}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class MareConfigurationAuthBase : MareConfigurationBase
|
||||
{
|
||||
public int DbContextPoolSize { get; set; } = 100;
|
||||
public int FailedAuthForTempBan { get; set; } = 5;
|
||||
public int TempBanDurationInMinutes { get; set; } = 5;
|
||||
public List<string> WhitelistedIps { get; set; } = new();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine($"{nameof(FailedAuthForTempBan)} => {FailedAuthForTempBan}");
|
||||
sb.AppendLine($"{nameof(TempBanDurationInMinutes)} => {TempBanDurationInMinutes}");
|
||||
sb.AppendLine($"{nameof(WhitelistedIps)} => {string.Join(", ", WhitelistedIps)}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace MareSynchronosShared.Utils;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class RemoteConfigurationAttribute : Attribute { }
|
||||
@@ -1,32 +1,39 @@
|
||||
using MareSynchronosShared.Utils;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronosServer;
|
||||
namespace MareSynchronosShared.Utils;
|
||||
|
||||
public class ServerConfiguration : MareConfigurationAuthBase
|
||||
{
|
||||
public Uri CdnFullUrl { get; set; } = null;
|
||||
public Uri ServiceAddress { get; set; } = null;
|
||||
public Uri StaticFileServiceAddress { get; set; } = null;
|
||||
public string RedisConnectionString { get; set; } = string.Empty;
|
||||
|
||||
[RemoteConfiguration]
|
||||
public Uri CdnFullUrl { get; set; } = null;
|
||||
[RemoteConfiguration]
|
||||
public Uri StaticFileServiceAddress { get; set; } = null;
|
||||
[RemoteConfiguration]
|
||||
public int MaxExistingGroupsByUser { get; set; } = 3;
|
||||
[RemoteConfiguration]
|
||||
public int MaxJoinedGroupsByUser { get; set; } = 6;
|
||||
[RemoteConfiguration]
|
||||
public int MaxGroupUserCount { get; set; } = 100;
|
||||
public string ShardName { get; set; } = string.Empty;
|
||||
[RemoteConfiguration]
|
||||
public bool PurgeUnusedAccounts { get; set; } = false;
|
||||
[RemoteConfiguration]
|
||||
public int PurgeUnusedAccountsPeriodInDays { get; set; } = 14;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine(base.ToString());
|
||||
sb.AppendLine($"{nameof(ShardName)} => {ShardName}");
|
||||
sb.AppendLine($"{nameof(MainServerGrpcAddress)} => {MainServerGrpcAddress}");
|
||||
sb.AppendLine($"{nameof(CdnFullUrl)} => {CdnFullUrl}");
|
||||
sb.AppendLine($"{nameof(ServiceAddress)} => {ServiceAddress}");
|
||||
sb.AppendLine($"{nameof(StaticFileServiceAddress)} => {StaticFileServiceAddress}");
|
||||
sb.AppendLine($"{nameof(RedisConnectionString)} => {RedisConnectionString}");
|
||||
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}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronosShared.Utils;
|
||||
|
||||
public class ServicesConfiguration : MareConfigurationBase
|
||||
{
|
||||
public string DiscordBotToken { get; set; } = string.Empty;
|
||||
public Uri MainServerGrpcAddress { get; set; } = null;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine(base.ToString());
|
||||
sb.AppendLine($"{nameof(DiscordBotToken)} => {DiscordBotToken}");
|
||||
sb.AppendLine($"{nameof(MainServerGrpcAddress)} => {MainServerGrpcAddress}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,68 @@
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronosShared.Utils;
|
||||
|
||||
public static class SharedDbFunctions
|
||||
{
|
||||
public static async Task PurgeUser(ILogger _logger, User user, MareDbContext dbContext, int maxGroupsByUser)
|
||||
{
|
||||
_logger.LogInformation("Purging user: {uid}", user.UID);
|
||||
|
||||
var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID);
|
||||
|
||||
if (lodestone != null)
|
||||
{
|
||||
dbContext.Remove(lodestone);
|
||||
}
|
||||
|
||||
var auth = dbContext.Auth.Single(a => a.UserUID == user.UID);
|
||||
|
||||
var userFiles = dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == user.UID).ToList();
|
||||
dbContext.Files.RemoveRange(userFiles);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
_logger.LogInformation("User purged: {uid}", user.UID);
|
||||
|
||||
dbContext.Auth.Remove(auth);
|
||||
dbContext.Users.Remove(user);
|
||||
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task<(bool, string)> MigrateOrDeleteGroup(MareDbContext context, Group group, List<GroupPair> groupPairs, int maxGroupsByUser)
|
||||
{
|
||||
bool groupHasMigrated = false;
|
||||
|
||||
@@ -3,18 +3,21 @@ using System.Text;
|
||||
|
||||
namespace MareSynchronosStaticFilesServer;
|
||||
|
||||
public class StaticFilesServerConfiguration : MareConfigurationAuthBase
|
||||
public class StaticFilesServerConfiguration : MareConfigurationBase
|
||||
{
|
||||
public Uri FileServerGrpcAddress { get; set; } = null;
|
||||
public int ForcedDeletionOfFilesAfterHours { get; set; } = -1;
|
||||
public double CacheSizeHardLimitInGiB { get; set; } = -1;
|
||||
public int UnusedFileRetentionPeriodInDays { get; set; } = -1;
|
||||
public int UnusedFileRetentionPeriodInDays { get; set; } = 14;
|
||||
public string CacheDirectory { get; set; }
|
||||
public Uri? RemoteCacheSourceUri { get; set; } = null;
|
||||
|
||||
public Uri MainServerGrpcAddress { get; set; } = null;
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine(base.ToString());
|
||||
sb.AppendLine($"{nameof(FileServerGrpcAddress)} => {FileServerGrpcAddress}");
|
||||
sb.AppendLine($"{nameof(MainServerGrpcAddress)} => {MainServerGrpcAddress}");
|
||||
sb.AppendLine($"{nameof(ForcedDeletionOfFilesAfterHours)} => {ForcedDeletionOfFilesAfterHours}");
|
||||
sb.AppendLine($"{nameof(CacheSizeHardLimitInGiB)} => {CacheSizeHardLimitInGiB}");
|
||||
sb.AppendLine($"{nameof(UnusedFileRetentionPeriodInDays)} => {UnusedFileRetentionPeriodInDays}");
|
||||
@@ -1,4 +1,5 @@
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Services;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
@@ -14,13 +15,13 @@ public class CachedFileProvider
|
||||
private readonly ConcurrentDictionary<string, Task> _currentTransfers = new(StringComparer.Ordinal);
|
||||
private bool IsMainServer => _remoteCacheSourceUri == null;
|
||||
|
||||
public CachedFileProvider(IOptions<StaticFilesServerConfiguration> configuration, ILogger<CachedFileProvider> logger, FileStatisticsService fileStatisticsService, MareMetrics metrics)
|
||||
public CachedFileProvider(IConfigurationService<StaticFilesServerConfiguration> configuration, ILogger<CachedFileProvider> logger, FileStatisticsService fileStatisticsService, MareMetrics metrics)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileStatisticsService = fileStatisticsService;
|
||||
_metrics = metrics;
|
||||
_remoteCacheSourceUri = configuration.Value.RemoteCacheSourceUri;
|
||||
_basePath = configuration.Value.CacheDirectory;
|
||||
_remoteCacheSourceUri = configuration.GetValueOrDefault<Uri>(nameof(StaticFilesServerConfiguration.RemoteCacheSourceUri), null);
|
||||
_basePath = configuration.GetValue<string>(nameof(StaticFilesServerConfiguration.CacheDirectory));
|
||||
}
|
||||
|
||||
public async Task<FileStream?> GetFileStream(string hash, string auth)
|
||||
@@ -79,6 +80,21 @@ public class CachedFileProvider
|
||||
|
||||
_fileStatisticsService.LogFile(hash, fi.Length);
|
||||
|
||||
return new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
int attempts = 0;
|
||||
while (attempts < 5)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
attempts++;
|
||||
_logger.LogWarning(ex, "Error opening file, retrying");
|
||||
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Could not open file " + fi.FullName);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Models;
|
||||
using MareSynchronosShared.Services;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosStaticFilesServer;
|
||||
@@ -11,19 +12,19 @@ public class FileCleanupService : IHostedService
|
||||
private readonly MareMetrics _metrics;
|
||||
private readonly ILogger<FileCleanupService> _logger;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly StaticFilesServerConfiguration _configuration;
|
||||
private readonly IConfigurationService<StaticFilesServerConfiguration> _configuration;
|
||||
private readonly bool _isMainServer;
|
||||
private readonly string _cacheDir;
|
||||
private CancellationTokenSource _cleanupCts;
|
||||
|
||||
public FileCleanupService(MareMetrics metrics, ILogger<FileCleanupService> logger, IServiceProvider services, IOptions<StaticFilesServerConfiguration> configuration)
|
||||
public FileCleanupService(MareMetrics metrics, ILogger<FileCleanupService> logger, IServiceProvider services, IConfigurationService<StaticFilesServerConfiguration> configuration)
|
||||
{
|
||||
_metrics = metrics;
|
||||
_logger = logger;
|
||||
_services = services;
|
||||
_configuration = configuration.Value;
|
||||
_isMainServer = _configuration.RemoteCacheSourceUri == null;
|
||||
_cacheDir = _configuration.CacheDirectory;
|
||||
_configuration = configuration;
|
||||
_isMainServer = configuration.IsMain;
|
||||
_cacheDir = _configuration.GetValue<string>(nameof(StaticFilesServerConfiguration.CacheDirectory));
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -60,26 +61,32 @@ public class FileCleanupService : IHostedService
|
||||
await dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogInformation("File Cleanup Complete, next run at {date}", DateTime.Now.Add(TimeSpan.FromMinutes(10)));
|
||||
await Task.Delay(TimeSpan.FromMinutes(10), ct).ConfigureAwait(false);
|
||||
var now = DateTime.Now;
|
||||
TimeOnly currentTime = new(now.Hour, now.Minute, now.Second);
|
||||
TimeOnly futureTime = new(now.Hour, now.Minute - now.Minute % 10, 0);
|
||||
var span = futureTime.AddMinutes(10) - currentTime;
|
||||
|
||||
_logger.LogInformation("File Cleanup Complete, next run at {date}", now.Add(span));
|
||||
await Task.Delay(span, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanUpFilesBeyondSizeLimit(MareDbContext dbContext, CancellationToken ct)
|
||||
{
|
||||
if (_configuration.CacheSizeHardLimitInGiB <= 0)
|
||||
var sizeLimit = _configuration.GetValueOrDefault<double>(nameof(StaticFilesServerConfiguration.CacheSizeHardLimitInGiB), -1);
|
||||
if (sizeLimit <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Cleaning up files beyond the cache size limit of {cacheSizeLimit} GiB", _configuration.CacheSizeHardLimitInGiB);
|
||||
_logger.LogInformation("Cleaning up files beyond the cache size limit of {cacheSizeLimit} GiB", sizeLimit);
|
||||
var allLocalFiles = Directory.EnumerateFiles(_cacheDir, "*", SearchOption.AllDirectories)
|
||||
.Select(f => new FileInfo(f)).ToList()
|
||||
.OrderBy(f => f.LastAccessTimeUtc).ToList();
|
||||
var totalCacheSizeInBytes = allLocalFiles.Sum(s => s.Length);
|
||||
long cacheSizeLimitInBytes = (long)ByteSize.FromGibiBytes(_configuration.CacheSizeHardLimitInGiB).Bytes;
|
||||
long cacheSizeLimitInBytes = (long)ByteSize.FromGibiBytes(sizeLimit).Bytes;
|
||||
while (totalCacheSizeInBytes > cacheSizeLimitInBytes && allLocalFiles.Any() && !ct.IsCancellationRequested)
|
||||
{
|
||||
var oldestFile = allLocalFiles[0];
|
||||
@@ -106,15 +113,18 @@ public class FileCleanupService : IHostedService
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Cleaning up files older than {filesOlderThanDays} days", _configuration.UnusedFileRetentionPeriodInDays);
|
||||
if (_configuration.ForcedDeletionOfFilesAfterHours > 0)
|
||||
var unusedRetention = _configuration.GetValueOrDefault<int>(nameof(StaticFilesServerConfiguration.UnusedFileRetentionPeriodInDays), 14);
|
||||
var forcedDeletionAfterHours = _configuration.GetValueOrDefault<int>(nameof(StaticFilesServerConfiguration.ForcedDeletionOfFilesAfterHours), -1);
|
||||
|
||||
_logger.LogInformation("Cleaning up files older than {filesOlderThanDays} days", unusedRetention);
|
||||
if (forcedDeletionAfterHours > 0)
|
||||
{
|
||||
_logger.LogInformation("Cleaning up files written to longer than {hours}h ago", _configuration.ForcedDeletionOfFilesAfterHours);
|
||||
_logger.LogInformation("Cleaning up files written to longer than {hours}h ago", forcedDeletionAfterHours);
|
||||
}
|
||||
|
||||
// clean up files in DB but not on disk or last access is expired
|
||||
var prevTime = DateTime.Now.Subtract(TimeSpan.FromDays(_configuration.UnusedFileRetentionPeriodInDays));
|
||||
var prevTimeForcedDeletion = DateTime.Now.Subtract(TimeSpan.FromHours(_configuration.ForcedDeletionOfFilesAfterHours));
|
||||
var prevTime = DateTime.Now.Subtract(TimeSpan.FromDays(unusedRetention));
|
||||
var prevTimeForcedDeletion = DateTime.Now.Subtract(TimeSpan.FromHours(forcedDeletionAfterHours));
|
||||
var allFiles = dbContext.Files.ToList();
|
||||
foreach (var fileCache in allFiles.Where(f => f.Uploaded))
|
||||
{
|
||||
@@ -133,7 +143,7 @@ public class FileCleanupService : IHostedService
|
||||
if (_isMainServer)
|
||||
dbContext.Files.Remove(fileCache);
|
||||
}
|
||||
else if (file != null && _configuration.ForcedDeletionOfFilesAfterHours > 0 && file.LastWriteTime < prevTimeForcedDeletion)
|
||||
else if (file != null && forcedDeletionAfterHours > 0 && file.LastWriteTime < prevTimeForcedDeletion)
|
||||
{
|
||||
_metrics.DecGauge(MetricsAPI.GaugeFilesTotalSize, file.Length);
|
||||
_metrics.DecGauge(MetricsAPI.GaugeFilesTotal);
|
||||
@@ -147,24 +157,7 @@ public class FileCleanupService : IHostedService
|
||||
}
|
||||
|
||||
// clean up files that are on disk but not in DB for some reason
|
||||
if (_isMainServer)
|
||||
{
|
||||
var allFilesHashes = new HashSet<string>(allFiles.Select(a => a.Hash.ToUpperInvariant()), StringComparer.Ordinal);
|
||||
DirectoryInfo dir = new(_cacheDir);
|
||||
var allFilesInDir = dir.GetFiles("*", SearchOption.AllDirectories);
|
||||
foreach (var file in allFilesInDir)
|
||||
{
|
||||
if (!allFilesHashes.Contains(file.Name.ToUpperInvariant()))
|
||||
{
|
||||
_metrics.DecGauge(MetricsAPI.GaugeFilesTotalSize, file.Length);
|
||||
_metrics.DecGauge(MetricsAPI.GaugeFilesTotal);
|
||||
file.Delete();
|
||||
_logger.LogInformation("File not in DB, deleting: {fileName}", file.Name);
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
CleanUpOrphanedFiles(allFiles, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -172,6 +165,28 @@ public class FileCleanupService : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanUpOrphanedFiles(List<FileCache> allFiles, CancellationToken ct)
|
||||
{
|
||||
if (_isMainServer)
|
||||
{
|
||||
var allFilesHashes = new HashSet<string>(allFiles.Select(a => a.Hash.ToUpperInvariant()), StringComparer.Ordinal);
|
||||
DirectoryInfo dir = new(_cacheDir);
|
||||
var allFilesInDir = dir.GetFiles("*", SearchOption.AllDirectories);
|
||||
foreach (var file in allFilesInDir)
|
||||
{
|
||||
if (!allFilesHashes.Contains(file.Name.ToUpperInvariant()))
|
||||
{
|
||||
_metrics.DecGauge(MetricsAPI.GaugeFilesTotalSize, file.Length);
|
||||
_metrics.DecGauge(MetricsAPI.GaugeFilesTotal);
|
||||
file.Delete();
|
||||
_logger.LogInformation("File not in DB, deleting: {fileName}", file.Name);
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cleanupCts.Cancel();
|
||||
|
||||
@@ -51,7 +51,13 @@ public class FileStatisticsService : IHostedService
|
||||
_pastHourFiles = new(StringComparer.Ordinal);
|
||||
_metrics.SetGaugeTo(MetricsAPI.GaugeFilesUniquePastHour, 0);
|
||||
_metrics.SetGaugeTo(MetricsAPI.GaugeFilesUniquePastHourSize, 0);
|
||||
await Task.Delay(TimeSpan.FromHours(1), _resetCancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
TimeOnly currentTime = new(now.Hour, now.Minute, now.Second);
|
||||
TimeOnly futureTime = new(now.Hour, 0, 0);
|
||||
var span = futureTime.AddHours(1) - currentTime;
|
||||
|
||||
await Task.Delay(span, _resetCancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +70,13 @@ public class FileStatisticsService : IHostedService
|
||||
_pastDayFiles = new(StringComparer.Ordinal);
|
||||
_metrics.SetGaugeTo(MetricsAPI.GaugeFilesUniquePastDay, 0);
|
||||
_metrics.SetGaugeTo(MetricsAPI.GaugeFilesUniquePastDaySize, 0);
|
||||
await Task.Delay(TimeSpan.FromDays(1), _resetCancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
TimeOnly currentTime = new(now.Hour, now.Minute, now.Second);
|
||||
TimeOnly futureTime = new(0, 0, 0);
|
||||
var span = futureTime - currentTime;
|
||||
|
||||
await Task.Delay(span, _resetCancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public class FilesController : Controller
|
||||
[HttpGet("{fileId}")]
|
||||
public async Task<IActionResult> GetFile(string fileId)
|
||||
{
|
||||
var authedUser = HttpContext.User.Claims.FirstOrDefault(f => string.Equals(f.Type, ClaimTypes.NameIdentifier, System.StringComparison.Ordinal))?.Value ?? "Unknown";
|
||||
var authedUser = HttpContext.User.Claims.FirstOrDefault(f => string.Equals(f.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value ?? "Unknown";
|
||||
_logger.LogInformation($"GetFile:{authedUser}:{fileId}");
|
||||
|
||||
var fs = await _cachedFileProvider.GetFileStream(fileId, Request.Headers["Authorization"]);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Protos;
|
||||
using MareSynchronosShared.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosStaticFilesServer;
|
||||
|
||||
@@ -14,9 +14,9 @@ public class GrpcFileService : FileService.FileServiceBase
|
||||
private readonly ILogger<GrpcFileService> _logger;
|
||||
private readonly MareMetrics _metricsClient;
|
||||
|
||||
public GrpcFileService(MareDbContext mareDbContext, IOptions<StaticFilesServerConfiguration> configuration, ILogger<GrpcFileService> logger, MareMetrics metricsClient)
|
||||
public GrpcFileService(MareDbContext mareDbContext, IConfigurationService<StaticFilesServerConfiguration> configuration, ILogger<GrpcFileService> logger, MareMetrics metricsClient)
|
||||
{
|
||||
_basePath = configuration.Value.CacheDirectory;
|
||||
_basePath = configuration.GetValue<string>(nameof(StaticFilesServerConfiguration.CacheDirectory));
|
||||
_mareDbContext = mareDbContext;
|
||||
_logger = logger;
|
||||
_metricsClient = metricsClient;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosStaticFilesServer;
|
||||
@@ -11,10 +13,13 @@ public class Program
|
||||
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var options = host.Services.GetService<IOptions<StaticFilesServerConfiguration>>();
|
||||
var options = host.Services.GetService<IConfigurationService<StaticFilesServerConfiguration>>();
|
||||
var optionsServer = host.Services.GetService<IConfigurationService<MareConfigurationAuthBase>>();
|
||||
var logger = host.Services.GetService<ILogger<Program>>();
|
||||
logger.LogInformation("Loaded MareSynchronos Static Files Server Configuration");
|
||||
logger.LogInformation(options.Value.ToString());
|
||||
logger.LogInformation("Loaded MareSynchronos Static Files Server Configuration (IsMain: {isMain})", options.IsMain);
|
||||
logger.LogInformation(options.ToString());
|
||||
logger.LogInformation("Loaded MareSynchronos Server Auth Configuration (IsMain: {isMain})", optionsServer.IsMain);
|
||||
logger.LogInformation(optionsServer.ToString());
|
||||
}
|
||||
|
||||
host.Run();
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
using Grpc.Net.Client.Configuration;
|
||||
using Grpc.Net.ClientFactory;
|
||||
using MareSynchronosShared.Authentication;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Protos;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Prometheus;
|
||||
|
||||
namespace MareSynchronosStaticFilesServer;
|
||||
@@ -27,11 +32,11 @@ public class Startup
|
||||
|
||||
services.AddLogging();
|
||||
|
||||
services.Configure<MareConfigurationAuthBase>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
services.Configure<StaticFilesServerConfiguration>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
services.Configure<MareConfigurationAuthBase>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
services.AddSingleton(Configuration);
|
||||
|
||||
var mareSettings = Configuration.GetRequiredSection("MareSynchronos");
|
||||
var mareConfig = Configuration.GetRequiredSection("MareSynchronos");
|
||||
|
||||
services.AddControllers();
|
||||
|
||||
@@ -64,7 +69,37 @@ public class Startup
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, mareSettings.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
|
||||
}, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
|
||||
|
||||
var noRetryConfig = new MethodConfig
|
||||
{
|
||||
Names = { MethodName.Default },
|
||||
RetryPolicy = null
|
||||
};
|
||||
|
||||
services.AddGrpcClient<ConfigurationService.ConfigurationServiceClient>("FileServer", c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>(nameof(StaticFilesServerConfiguration.FileServerGrpcAddress)));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { noRetryConfig } };
|
||||
c.HttpHandler = new SocketsHttpHandler()
|
||||
{
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
});
|
||||
|
||||
services.AddGrpcClient<ConfigurationService.ConfigurationServiceClient>("MainServer", c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>(nameof(StaticFilesServerConfiguration.MainServerGrpcAddress)));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { noRetryConfig } };
|
||||
c.HttpHandler = new SocketsHttpHandler()
|
||||
{
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
@@ -73,10 +108,30 @@ public class Startup
|
||||
.AddScheme<AuthenticationSchemeOptions, SecretKeyAuthenticationHandler>(SecretKeyAuthenticationHandler.AuthScheme, options => { });
|
||||
services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
|
||||
|
||||
services.AddGrpc(o =>
|
||||
if (_isMain)
|
||||
{
|
||||
o.MaxReceiveMessageSize = null;
|
||||
});
|
||||
services.AddGrpc(o =>
|
||||
{
|
||||
o.MaxReceiveMessageSize = null;
|
||||
});
|
||||
|
||||
services.AddSingleton<IConfigurationService<StaticFilesServerConfiguration>, MareConfigurationServiceServer<StaticFilesServerConfiguration>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IConfigurationService<StaticFilesServerConfiguration>>(p => new MareConfigurationServiceClient<StaticFilesServerConfiguration>(
|
||||
p.GetRequiredService<ILogger<MareConfigurationServiceClient<StaticFilesServerConfiguration>>>(),
|
||||
p.GetRequiredService<IOptions<StaticFilesServerConfiguration>>(),
|
||||
p.GetRequiredService<GrpcClientFactory>(),
|
||||
"FileServer"));
|
||||
}
|
||||
|
||||
services.AddSingleton<IConfigurationService<MareConfigurationAuthBase>>(p =>
|
||||
new MareConfigurationServiceClient<MareConfigurationAuthBase>(
|
||||
p.GetRequiredService<ILogger<MareConfigurationServiceClient<MareConfigurationAuthBase>>>(),
|
||||
p.GetService<IOptions<MareConfigurationAuthBase>>(),
|
||||
p.GetRequiredService<GrpcClientFactory>(), "MainServer")
|
||||
);
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
@@ -85,7 +140,9 @@ public class Startup
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
var metricServer = new KestrelMetricServer(4981);
|
||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationAuthBase>>();
|
||||
|
||||
var metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(MareConfigurationBase.MetricsPort), 4981));
|
||||
metricServer.Start();
|
||||
|
||||
app.UseHttpMetrics();
|
||||
@@ -95,8 +152,10 @@ public class Startup
|
||||
|
||||
app.UseEndpoints(e =>
|
||||
{
|
||||
if(_isMain)
|
||||
if (_isMain)
|
||||
{
|
||||
e.MapGrpcService<GrpcFileService>();
|
||||
}
|
||||
e.MapControllers();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user