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:
rootdarkarchon
2022-12-27 13:48:05 +01:00
committed by GitHub
parent 7ee7fdaf48
commit 9eb5967935
101 changed files with 2470 additions and 585 deletions

View File

@@ -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")]

View File

@@ -1,6 +1,4 @@
using MareSynchronos.API;
using System.Threading.Tasks;
using System;
namespace MareSynchronosServer.Hubs
{

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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()

View File

@@ -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;

View File

@@ -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

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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()

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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();
}
});
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MareSynchronosServer.Utils;
namespace MareSynchronosServer.Utils;
public record PausedEntry
{

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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()

View File

@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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>();
});
}
}

View File

@@ -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;

View File

@@ -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))
{

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Http;
namespace MareSynchronosServer;
namespace MareSynchronosShared;
public static class Extensions
{

View File

@@ -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 {

View File

@@ -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 });
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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}");

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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"]);

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();
});
}