diff --git a/MareSynchronosServer/MareSynchronosServer.sln b/MareSynchronosServer/MareSynchronosServer.sln index 461354f..4e7c962 100644 --- a/MareSynchronosServer/MareSynchronosServer.sln +++ b/MareSynchronosServer/MareSynchronosServer.sln @@ -9,7 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "..\Ma EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServerTest", "MareSynchronosServerTest\MareSynchronosServerTest.csproj", "{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosShared", "MareSynchronosShared\MareSynchronosShared.csproj", "{67B1461D-E215-4BA8-A64D-E1836724D5E6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosShared", "MareSynchronosShared\MareSynchronosShared.csproj", "{67B1461D-E215-4BA8-A64D-E1836724D5E6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosStaticFilesServer", "MareSynchronosStaticFilesServer\MareSynchronosStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosServices", "MareSynchronosServices\MareSynchronosServices.csproj", "{E29C8677-AB44-4950-9EB1-D8E70B710A56}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,6 +37,14 @@ Global {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.Build.0 = Release|Any CPU + {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.Build.0 = Release|Any CPU + {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs b/MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs deleted file mode 100644 index 46b5887..0000000 --- a/MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs +++ /dev/null @@ -1,431 +0,0 @@ -using Discord; -using Discord.WebSocket; -using MareSynchronosServer.Hubs; -using MareSynchronosServer.Metrics; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Net.Http; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using MareSynchronosShared.Data; -using MareSynchronosShared.Models; - -namespace MareSynchronosServer.Discord -{ - public class DiscordBot : IHostedService - { - private readonly IServiceProvider services; - private readonly IConfiguration configuration; - private readonly ILogger logger; - private readonly Random random; - private string authToken = string.Empty; - DiscordSocketClient discordClient; - ConcurrentDictionary DiscordLodestoneMapping = new(); - private CancellationTokenSource verificationTaskCts; - private CancellationTokenSource updateStatusCts; - private readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; - private readonly ConcurrentQueue verificationQueue = new(); - - private SemaphoreSlim semaphore; - - public DiscordBot(IServiceProvider services, IConfiguration configuration, ILogger logger) - { - this.services = services; - this.configuration = configuration; - this.logger = logger; - this.verificationQueue = new ConcurrentQueue(); - this.semaphore = new SemaphoreSlim(1); - - random = new(); - authToken = configuration.GetValue("DiscordBotToken"); - - discordClient = new(new DiscordSocketConfig() - { - DefaultRetryMode = RetryMode.AlwaysRetry - }); - - discordClient.Log += Log; - } - - private async Task DiscordClient_SlashCommandExecuted(SocketSlashCommand arg) - { - await semaphore.WaitAsync().ConfigureAwait(false); - try - { - if (arg.Data.Name == "register") - { - if (arg.Data.Options.FirstOrDefault(f => f.Name == "overwrite_old_account") != null) - { - await DeletePreviousUserAccount(arg.User.Id).ConfigureAwait(false); - } - - var modal = new ModalBuilder(); - modal.WithTitle("Verify with Lodestone"); - modal.WithCustomId("register_modal"); - modal.AddTextInput("Enter the Lodestone URL of your Character", "lodestoneurl", TextInputStyle.Short, "https://*.finalfantasyxiv.com/lodestone/character//", required: true); - await arg.RespondWithModalAsync(modal.Build()).ConfigureAwait(false); - } - else if (arg.Data.Name == "verify") - { - EmbedBuilder eb = new(); - if (verificationQueue.Any(u => u.User.Id == arg.User.Id)) - { - eb.WithTitle("Already queued for verfication"); - eb.WithDescription("You are already queued for verification. Please wait."); - await arg.RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); - } - else if (!DiscordLodestoneMapping.ContainsKey(arg.User.Id)) - { - eb.WithTitle("Cannot verify registration"); - eb.WithDescription("You need to **/register** first before you can **/verify**"); - await arg.RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); - } - else - { - await arg.DeferAsync(ephemeral: true).ConfigureAwait(false); - verificationQueue.Enqueue(arg); - } - } - else - { - await arg.RespondAsync("idk what you did to get here to start, just follow the instructions as provided.", ephemeral: true).ConfigureAwait(false); - } - } - finally - { - semaphore.Release(); - } - } - - private async Task DeletePreviousUserAccount(ulong id) - { - using var scope = services.CreateScope(); - using var db = scope.ServiceProvider.GetService(); - var discordAuthedUser = await db.LodeStoneAuth.Include(u => u.User).FirstOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false); - if (discordAuthedUser != null) - { - if (discordAuthedUser.User != null) - { - CleanupService.PurgeUser(discordAuthedUser.User, db, configuration); - } - else - { - db.Remove(discordAuthedUser); - } - - await db.SaveChangesAsync().ConfigureAwait(false); - } - } - - private async Task DiscordClient_ModalSubmitted(SocketModal arg) - { - if (arg.Data.CustomId == "register_modal") - { - var embed = await HandleRegisterModalAsync(arg).ConfigureAwait(false); - await arg.RespondAsync(embeds: new Embed[] { embed }, ephemeral: true).ConfigureAwait(false); - } - } - - private async Task HandleVerifyAsync(ulong id) - { - var embedBuilder = new EmbedBuilder(); - - using var scope = services.CreateScope(); - var req = new HttpClient(); - using var db = scope.ServiceProvider.GetService(); - - var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == id); - if (lodestoneAuth != null && DiscordLodestoneMapping.ContainsKey(id)) - { - var randomServer = LodestoneServers[random.Next(LodestoneServers.Length)]; - var response = await req.GetAsync($"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{DiscordLodestoneMapping[id]}").ConfigureAwait(false); - if (response.IsSuccessStatusCode) - { - var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - if (content.Contains(lodestoneAuth.LodestoneAuthString)) - { - DiscordLodestoneMapping.TryRemove(id, out _); - - using var sha256 = SHA256.Create(); - var user = new User(); - - var hasValidUid = false; - while (!hasValidUid) - { - var uid = MareHub.GenerateRandomString(10); - if (db.Users.Any(u => u.UID == uid)) continue; - user.UID = uid; - hasValidUid = true; - } - - // make the first registered user on the service to admin - if (!await db.Users.AnyAsync().ConfigureAwait(false)) - { - user.IsAdmin = true; - } - - if (configuration.GetValue("PurgeUnusedAccounts")) - { - var purgedDays = configuration.GetValue("PurgeUnusedAccountsPeriodInDays"); - user.LastLoggedIn = DateTime.UtcNow - TimeSpan.FromDays(purgedDays) + TimeSpan.FromDays(1); - } - - var computedHash = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(MareHub.GenerateRandomString(64)))).Replace("-", ""); - var auth = new Auth() - { - HashedKey = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(computedHash))) - .Replace("-", ""), - User = user, - }; - - await db.Users.AddAsync(user).ConfigureAwait(false); - await db.Auth.AddAsync(auth).ConfigureAwait(false); - - logger.LogInformation("User registered: {userUID}", user.UID); - - MareMetrics.UsersRegistered.Inc(); - - lodestoneAuth.StartedAt = null; - lodestoneAuth.User = user; - lodestoneAuth.LodestoneAuthString = null; - - embedBuilder.WithTitle("Registration successful"); - embedBuilder.WithDescription("This is your private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**" - + Environment.NewLine + Environment.NewLine - + $"**{computedHash}**" - + Environment.NewLine + Environment.NewLine - + "Enter this key in Mare Synchronos and hit save to connect to the service." - + Environment.NewLine - + "You should connect as soon as possible to not get caught by the automatic cleanup process." - + Environment.NewLine - + "Have fun."); - } - else - { - embedBuilder.WithTitle("Failed to verify your character"); - embedBuilder.WithDescription("Did not find requested authentication key on your profile. Make sure you have saved *twice*, then do **/verify** again."); - lodestoneAuth.StartedAt = DateTime.UtcNow; - } - } - - await db.SaveChangesAsync().ConfigureAwait(false); - } - else - { - embedBuilder.WithTitle("Your auth has expired or something else went wrong"); - embedBuilder.WithDescription("Start again with **/register**"); - DiscordLodestoneMapping.TryRemove(id, out _); - } - - return embedBuilder.Build(); - } - - private async Task HandleRegisterModalAsync(SocketModal arg) - { - var embed = new EmbedBuilder(); - - var lodestoneId = ParseCharacterIdFromLodestoneUrl(arg.Data.Components.Single(c => c.CustomId == "lodestoneurl").Value); - if (lodestoneId == null) - { - embed.WithTitle("Invalid Lodestone URL"); - embed.WithDescription("The lodestone URL was not valid. It should have following format:" + Environment.NewLine - + "https://eu.finalfantasyxiv.com/lodestone/character/YOUR_LODESTONE_ID/"); - } - else - { - // check if userid is already in db - using var scope = services.CreateScope(); - using var sha256 = SHA256.Create(); - - var hashedLodestoneId = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(lodestoneId.ToString()))).Replace("-", ""); - - using var db = scope.ServiceProvider.GetService(); - - // check if discord id or lodestone id is banned - if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == arg.User.Id.ToString() || a.DiscordIdOrLodestoneAuth == hashedLodestoneId)) - { - embed.WithTitle("no"); - embed.WithDescription("your account is banned"); - } - else if (db.LodeStoneAuth.Any(a => a.DiscordId == arg.User.Id)) - { - // user already in db - embed.WithTitle("Registration failed"); - embed.WithDescription("You cannot register more than one lodestone character to your discord account."); - } - else if (db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId)) - { - // character already in db - embed.WithTitle("Registration failed"); - embed.WithDescription("This lodestone character already exists in the Database. If you are the rightful owner for this character and lost your secret key generated with it, contact the developer."); - } - else - { - string lodestoneAuth = await GenerateLodestoneAuth(arg.User.Id, hashedLodestoneId, db).ConfigureAwait(false); - // check if lodestone id is already in db - embed.WithTitle("Authorize your character"); - embed.WithDescription("Add following key to your character profile at https://na.finalfantasyxiv.com/lodestone/my/setting/profile/" - + Environment.NewLine + Environment.NewLine - + $"**{lodestoneAuth}**" - + Environment.NewLine + Environment.NewLine - + $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN MARE !**" - + Environment.NewLine + Environment.NewLine - + "Once added and saved, use command **/verify** to finish registration and receive a secret key to use for Mare Synchronos." - + Environment.NewLine - + "You can delete the entry from your profile after verification." - + Environment.NewLine + Environment.NewLine - + "The verification will expire in approximately 15 minutes. If you fail to **/verify** the registration will be invalidated and you have to **/register** again."); - DiscordLodestoneMapping[arg.User.Id] = lodestoneId.ToString(); - } - } - - return embed.Build(); - } - - private async Task GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, MareDbContext dbContext) - { - var auth = MareHub.GenerateRandomString(32); - LodeStoneAuth lsAuth = new LodeStoneAuth() - { - DiscordId = discordid, - HashedLodestoneId = hashedLodestoneId, - LodestoneAuthString = auth, - StartedAt = DateTime.UtcNow - }; - - dbContext.Add(lsAuth); - await dbContext.SaveChangesAsync().ConfigureAwait(false); - - return auth; - } - - private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl) - { - var regex = new Regex(@"https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/\d+"); - var matches = regex.Match(lodestoneUrl); - var isLodestoneUrl = matches.Success; - if (!isLodestoneUrl || matches.Groups.Count < 1) return null; - - lodestoneUrl = matches.Groups[0].ToString(); - var stringId = lodestoneUrl.Split('/', StringSplitOptions.RemoveEmptyEntries).Last(); - if (!int.TryParse(stringId, out int lodestoneId)) - { - return null; - } - - return lodestoneId; - } - - private async Task DiscordClient_Ready() - { - var register = new SlashCommandBuilder() - .WithName("register") - .WithDescription("Registration for the Mare Synchronos server of this Discord") - .AddOption(new SlashCommandOptionBuilder() - .WithName("new_account") - .WithDescription("Starts the registration process for the Mare Synchronos server of this Discord") - .WithType(ApplicationCommandOptionType.SubCommand)) - .AddOption(new SlashCommandOptionBuilder() - .WithName("overwrite_old_account") - .WithDescription("Will forcefully overwrite your current character on the service, if present") - .WithType(ApplicationCommandOptionType.SubCommand)); - - var verify = new SlashCommandBuilder(); - verify.WithName("verify"); - verify.WithDescription("Finishes the registration process for the Mare Synchronos server of this Discord"); - - try - { - await discordClient.CreateGlobalApplicationCommandAsync(register.Build()).ConfigureAwait(false); - await discordClient.CreateGlobalApplicationCommandAsync(verify.Build()).ConfigureAwait(false); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to create command"); - } - } - - private Task Log(LogMessage msg) - { - logger.LogInformation("{msg}", msg); - - return Task.CompletedTask; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - if (!string.IsNullOrEmpty(authToken)) - { - authToken = configuration.GetValue("DiscordBotToken"); - - await discordClient.LoginAsync(TokenType.Bot, authToken).ConfigureAwait(false); - await discordClient.StartAsync().ConfigureAwait(false); - - discordClient.Ready += DiscordClient_Ready; - discordClient.SlashCommandExecuted += DiscordClient_SlashCommandExecuted; - discordClient.ModalSubmitted += DiscordClient_ModalSubmitted; - - _ = ProcessQueueWork(); - _ = UpdateStatusAsync(); - } - } - - private async Task ProcessQueueWork() - { - verificationTaskCts = new CancellationTokenSource(); - while (!verificationTaskCts.IsCancellationRequested) - { - if (verificationQueue.TryDequeue(out var queueitem)) - { - try - { - var dataEmbed = await HandleVerifyAsync(queueitem.User.Id).ConfigureAwait(false); - await queueitem.FollowupAsync(embed: dataEmbed, ephemeral: true).ConfigureAwait(false); - - logger.LogInformation("Sent login information to user"); - } - catch (Exception e) - { - logger.LogError(e, "Error during queue work"); - } - - } - await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).ConfigureAwait(false); - } - } - - private async Task UpdateStatusAsync() - { - updateStatusCts = new(); - while (!updateStatusCts.IsCancellationRequested) - { - await using var scope = services.CreateAsyncScope(); - await using var db = scope.ServiceProvider.GetService(); - - var users = db.Users.Count(c => c.CharacterIdentification != null); - - await discordClient.SetActivityAsync(new Game("Mare for " + users + " Users")).ConfigureAwait(false); - - await Task.Delay(TimeSpan.FromSeconds(15)).ConfigureAwait(false); - } - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - verificationTaskCts?.Cancel(); - updateStatusCts?.Cancel(); - - await discordClient.LogoutAsync().ConfigureAwait(false); - await discordClient.StopAsync().ConfigureAwait(false); - } - } -} diff --git a/MareSynchronosServer/MareSynchronosServer/IdBasedUserIdProvider.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/IdBasedUserIdProvider.cs similarity index 90% rename from MareSynchronosServer/MareSynchronosServer/IdBasedUserIdProvider.cs rename to MareSynchronosServer/MareSynchronosServer/Hubs/IdBasedUserIdProvider.cs index 2da4d53..ef08d56 100644 --- a/MareSynchronosServer/MareSynchronosServer/IdBasedUserIdProvider.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/IdBasedUserIdProvider.cs @@ -2,7 +2,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.SignalR; -namespace MareSynchronosServer +namespace MareSynchronosServer.Hubs { public class IdBasedUserIdProvider : IUserIdProvider { diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs index 23f5271..4600284 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs @@ -18,7 +18,7 @@ namespace MareSynchronosServer.Hubs private List OnlineAdmins => _dbContext.Users.Where(u => !string.IsNullOrEmpty(u.CharacterIdentification) && (u.IsModerator || u.IsAdmin)) .Select(u => u.UID).ToList(); - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendAdminChangeModeratorStatus)] public async Task ChangeModeratorStatus(string uid, bool isModerator) { @@ -33,7 +33,7 @@ namespace MareSynchronosServer.Hubs await Clients.Users(user.UID).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendAdminDeleteBannedUser)] public async Task DeleteBannedUser(BannedUserDto dto) { @@ -51,7 +51,7 @@ namespace MareSynchronosServer.Hubs await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteBannedUser, dto).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendAdminDeleteForbiddenFile)] public async Task DeleteForbiddenFile(ForbiddenFileDto dto) { @@ -69,7 +69,7 @@ namespace MareSynchronosServer.Hubs await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteForbiddenFile, dto).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeAdminGetBannedUsers)] public async Task> GetBannedUsers() { @@ -82,7 +82,7 @@ namespace MareSynchronosServer.Hubs }).ToListAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeAdminGetForbiddenFiles)] public async Task> GetForbiddenFiles() { @@ -95,7 +95,7 @@ namespace MareSynchronosServer.Hubs }).ToListAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeAdminGetOnlineUsers)] public async Task> AdminGetOnlineUsers() { @@ -110,7 +110,7 @@ namespace MareSynchronosServer.Hubs }).ToListAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendAdminUpdateOrAddBannedUser)] public async Task UpdateOrAddBannedUser(BannedUserDto dto) { @@ -142,7 +142,7 @@ namespace MareSynchronosServer.Hubs } } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendAdminUpdateOrAddForbiddenFile)] public async Task UpdateOrAddForbiddenFile(ForbiddenFileDto dto) { diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs index 2f31a0c..d4e7396 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs @@ -7,9 +7,10 @@ using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using MareSynchronos.API; -using MareSynchronosServer.Metrics; using MareSynchronosShared.Authentication; +using MareSynchronosShared.Metrics; using MareSynchronosShared.Models; +using MareSynchronosShared.Protos; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; @@ -21,7 +22,7 @@ namespace MareSynchronosServer.Hubs { private string BasePath => _configuration["CacheDirectory"]; - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendFileAbortUpload)] public async Task AbortUpload() { @@ -32,7 +33,7 @@ namespace MareSynchronosServer.Hubs await _dbContext.SaveChangesAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendFileDeleteAllFiles)] public async Task DeleteAllFiles() { @@ -44,8 +45,10 @@ namespace MareSynchronosServer.Hubs var fi = new FileInfo(Path.Combine(BasePath, file.Hash)); if (fi.Exists) { - MareMetrics.FilesTotalSize.Dec(fi.Length); - MareMetrics.FilesTotal.Dec(); + await _metricsClient.DecGaugeAsync(new GaugeRequest() + {GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = fi.Length}).ConfigureAwait(false); + await _metricsClient.DecGaugeAsync(new GaugeRequest() + { GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1}).ConfigureAwait(false); fi.Delete(); } } @@ -53,7 +56,7 @@ namespace MareSynchronosServer.Hubs await _dbContext.SaveChangesAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeGetFilesSizes)] public async Task> GetFilesSizes(List hashes) { @@ -97,7 +100,7 @@ namespace MareSynchronosServer.Hubs return response; } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeFileIsUploadFinished)] public async Task IsUploadFinished() { @@ -106,7 +109,7 @@ namespace MareSynchronosServer.Hubs .AnyAsync(f => f.Uploader.UID == userUid && !f.Uploaded).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeFileSendFiles)] public async Task> SendFiles(List fileListHashes) { @@ -156,7 +159,7 @@ namespace MareSynchronosServer.Hubs return notCoveredFiles.Values.ToList(); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendFileUploadFileStreamAsync)] public async Task UploadFileStreamAsync(string hash, IAsyncEnumerable fileContent) { @@ -224,8 +227,10 @@ namespace MareSynchronosServer.Hubs relatedFile = _dbContext.Files.Single(f => f.Hash == hash); relatedFile.Uploaded = true; - MareMetrics.FilesTotal.Inc(); - MareMetrics.FilesTotalSize.Inc(length); + await _metricsClient.IncGaugeAsync(new GaugeRequest() + { GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = length }).ConfigureAwait(false); + await _metricsClient.IncGaugeAsync(new GaugeRequest() + { GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1 }).ConfigureAwait(false); await _dbContext.SaveChangesAsync().ConfigureAwait(false); _logger.LogInformation("File {hash} added to DB", hash); diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs index 961756c..8ad64de 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs @@ -2,9 +2,10 @@ using System.Linq; using System.Threading.Tasks; using MareSynchronos.API; -using MareSynchronosServer.Metrics; using MareSynchronosShared.Authentication; +using MareSynchronosShared.Metrics; using MareSynchronosShared.Models; +using MareSynchronosShared.Protos; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; @@ -14,7 +15,7 @@ namespace MareSynchronosServer.Hubs { public partial class MareHub { - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendUserDeleteAccount)] public async Task DeleteAccount() { @@ -37,10 +38,8 @@ namespace MareSynchronosServer.Hubs await Task.Delay(1000).ConfigureAwait(false); } - SecretKeyAuthenticationHandler.RemoveAuthentication(userid); + await _authServiceClient.RemoveAuthAsync(new RemoveAuthRequest() { Uid = userid }).ConfigureAwait(false); - MareMetrics.Pairs.Dec(ownPairData.Count); - MareMetrics.PairsPaused.Dec(ownPairData.Count(c => c.IsPaused)); _dbContext.RemoveRange(ownPairData); await _dbContext.SaveChangesAsync().ConfigureAwait(false); @@ -56,9 +55,12 @@ namespace MareSynchronosServer.Hubs }, userEntry.CharacterIdentification).ConfigureAwait(false); } - MareMetrics.Pairs.Dec(otherPairData.Count); - MareMetrics.PairsPaused.Dec(otherPairData.Count(c => c.IsPaused)); - MareMetrics.UsersRegistered.Dec(); + await _metricsClient.DecGaugeAsync(new GaugeRequest() + { GaugeName = MetricsAPI.GaugePairs, Value = ownPairData.Count + otherPairData.Count }).ConfigureAwait(false); + await _metricsClient.DecGaugeAsync(new GaugeRequest() + { GaugeName = MetricsAPI.GaugePairsPaused, Value = ownPairData.Count(c => c.IsPaused) }).ConfigureAwait(false); + await _metricsClient.DecGaugeAsync(new GaugeRequest() + { GaugeName = MetricsAPI.GaugeUsersRegistered, Value = 1 }).ConfigureAwait(false); _dbContext.RemoveRange(otherPairData); _dbContext.Remove(userEntry); @@ -66,7 +68,7 @@ namespace MareSynchronosServer.Hubs await _dbContext.SaveChangesAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeUserGetOnlineCharacters)] public async Task> GetOnlineCharacters() { @@ -88,7 +90,7 @@ namespace MareSynchronosServer.Hubs return otherEntries.Select(e => e.User.CharacterIdentification).Distinct().ToList(); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeUserGetPairedClients)] public async Task> GetPairedClients() { @@ -126,7 +128,7 @@ namespace MareSynchronosServer.Hubs }).ToList(); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeUserPushCharacterDataToVisibleClients)] public async Task PushCharacterDataToVisibleClients(CharacterCacheDto characterCache, List visibleCharacterIds) { @@ -158,11 +160,13 @@ namespace MareSynchronosServer.Hubs await Clients.Users(otherEntries).SendAsync(Api.OnUserReceiveCharacterData, characterCache, user.CharacterIdentification).ConfigureAwait(false); - MareMetrics.UserPushData.Inc(); - MareMetrics.UserPushDataTo.Inc(otherEntries.Count); + await _metricsClient.IncreaseCounterAsync(new IncreaseCounterRequest() + { CounterName = MetricsAPI.CounterUserPushData, Value = 1 }).ConfigureAwait(false); + await _metricsClient.IncreaseCounterAsync(new IncreaseCounterRequest() + { CounterName = MetricsAPI.CounterUserPushDataTo, Value = otherEntries.Count }).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendUserPairedClientAddition)] public async Task SendPairedClientAddition(string uid) { @@ -215,10 +219,10 @@ namespace MareSynchronosServer.Hubs } } - MareMetrics.Pairs.Inc(); + await _metricsClient.IncGaugeAsync(new GaugeRequest() {GaugeName = MetricsAPI.GaugePairs, Value = 1}).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendUserPairedClientPauseChange)] public async Task SendPairedClientPauseChange(string otherUserUid, bool isPaused) { @@ -255,15 +259,15 @@ namespace MareSynchronosServer.Hubs if (isPaused) { - MareMetrics.PairsPaused.Inc(); + await _metricsClient.IncGaugeAsync(new GaugeRequest() { GaugeName = MetricsAPI.GaugePairsPaused, Value = 1 }).ConfigureAwait(false); } else { - MareMetrics.PairsPaused.Dec(); + await _metricsClient.DecGaugeAsync(new GaugeRequest() { GaugeName = MetricsAPI.GaugePairsPaused, Value = 1 }).ConfigureAwait(false); } } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendUserPairedClientRemoval)] public async Task SendPairedClientRemoval(string uid) { @@ -303,7 +307,7 @@ namespace MareSynchronosServer.Hubs } } - MareMetrics.Pairs.Dec(); + await _metricsClient.DecGaugeAsync(new GaugeRequest() { GaugeName = MetricsAPI.GaugePairs, Value = 1 }).ConfigureAwait(false); } private ClientPair OppositeEntry(string otherUID) => diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs index e98aa6a..413702c 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs @@ -4,10 +4,11 @@ using System.Security.Claims; using System.Security.Cryptography; using System.Threading.Tasks; using MareSynchronos.API; -using MareSynchronosServer.Metrics; using MareSynchronosShared.Authentication; using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; using MareSynchronosShared.Models; +using MareSynchronosShared.Protos; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; @@ -19,14 +20,19 @@ namespace MareSynchronosServer.Hubs { public partial class MareHub : Hub { + private readonly MetricsService.MetricsServiceClient _metricsClient; + private readonly AuthService.AuthServiceClient _authServiceClient; private readonly SystemInfoService _systemInfoService; private readonly IConfiguration _configuration; private readonly IHttpContextAccessor contextAccessor; private readonly ILogger _logger; private readonly MareDbContext _dbContext; - public MareHub(MareDbContext mareDbContext, ILogger logger, SystemInfoService systemInfoService, IConfiguration configuration, IHttpContextAccessor contextAccessor) + public MareHub(MetricsService.MetricsServiceClient metricsClient, AuthService.AuthServiceClient authServiceClient, + MareDbContext mareDbContext, ILogger logger, SystemInfoService systemInfoService, IConfiguration configuration, IHttpContextAccessor contextAccessor) { + _metricsClient = metricsClient; + _authServiceClient = authServiceClient; _systemInfoService = systemInfoService; _configuration = configuration; this.contextAccessor = contextAccessor; @@ -35,10 +41,10 @@ namespace MareSynchronosServer.Hubs } [HubMethodName(Api.InvokeHeartbeat)] - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] public async Task Heartbeat(string characterIdentification) { - MareMetrics.InitializedConnections.Inc(); + await _metricsClient.IncreaseCounterAsync(new() { CounterName = MetricsAPI.CounterInitializedConnections, Value = 1 }).ConfigureAwait(false); var userId = Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; @@ -60,7 +66,7 @@ namespace MareSynchronosServer.Hubs } else if (string.IsNullOrEmpty(user.CharacterIdentification)) { - MareMetrics.AuthorizedConnections.Inc(); + await _metricsClient.IncGaugeAsync(new GaugeRequest() { GaugeName = MetricsAPI.GaugeAuthorizedConnections, Value = 1 }).ConfigureAwait(false); } user.LastLoggedIn = DateTime.UtcNow; @@ -81,32 +87,34 @@ namespace MareSynchronosServer.Hubs }; } - public override Task OnConnectedAsync() + public override async Task OnConnectedAsync() { _logger.LogInformation("Connection from {ip}", contextAccessor.GetIpAddress()); - MareMetrics.Connections.Inc(); - return base.OnConnectedAsync(); + await _metricsClient.IncGaugeAsync(new GaugeRequest() { GaugeName = MetricsAPI.GaugeConnections, Value = 1 }).ConfigureAwait(false); + await base.OnConnectedAsync().ConfigureAwait(false); } public override async Task OnDisconnectedAsync(Exception exception) { - MareMetrics.Connections.Dec(); + await _metricsClient.DecGaugeAsync(new GaugeRequest() { GaugeName = MetricsAPI.GaugeConnections, Value = 1 }).ConfigureAwait(false); var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); if (user != null && !string.IsNullOrEmpty(user.CharacterIdentification)) { - MareMetrics.AuthorizedConnections.Dec(); + await _metricsClient.DecGaugeAsync(new GaugeRequest() { GaugeName = MetricsAPI.GaugeAuthorizedConnections, Value = 1 }).ConfigureAwait(false); _logger.LogInformation("Disconnect from {id}", AuthenticatedUserId); var query = from userToOther in _dbContext.ClientPairs join otherToUser in _dbContext.ClientPairs - on new { + on new + { user = userToOther.UserUID, other = userToOther.OtherUserUID - } equals new { + } equals new + { user = otherToUser.OtherUserUID, other = otherToUser.UserUID } @@ -118,7 +126,7 @@ namespace MareSynchronosServer.Hubs var otherEntries = await query.ToListAsync().ConfigureAwait(false); await Clients.Users(otherEntries).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, user.CharacterIdentification).ConfigureAwait(false); - + _dbContext.RemoveRange(_dbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == user.UID)); user.CharacterIdentification = null; @@ -128,24 +136,6 @@ namespace MareSynchronosServer.Hubs await base.OnDisconnectedAsync(exception).ConfigureAwait(false); } - public static string GenerateRandomString(int length, string allowableChars = null) - { - if (string.IsNullOrEmpty(allowableChars)) - allowableChars = @"ABCDEFGHJKLMNPQRSTUVWXYZ0123456789"; - - // Generate random data - var rnd = RandomNumberGenerator.GetBytes(length); - - // Generate the output string - var allowable = allowableChars.ToCharArray(); - var l = allowable.Length; - var chars = new char[length]; - for (var i = 0; i < length; i++) - chars[i] = allowable[rnd[i] % l]; - - return new string(chars); - } - protected string AuthenticatedUserId => Context.User?.Claims?.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "Unknown"; protected async Task GetAuthenticatedUserUntrackedAsync() diff --git a/MareSynchronosServer/MareSynchronosServer/Throttling/SignalRLimitFilter.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs similarity index 94% rename from MareSynchronosServer/MareSynchronosServer/Throttling/SignalRLimitFilter.cs rename to MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs index 2631928..b79846d 100644 --- a/MareSynchronosServer/MareSynchronosServer/Throttling/SignalRLimitFilter.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs @@ -1,22 +1,22 @@ -using AspNetCoreRateLimit; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; +using System; using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using AspNetCoreRateLimit; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; -namespace MareSynchronosServer.Throttling; +namespace MareSynchronosServer.Hubs; public class SignalRLimitFilter : IHubFilter { private readonly IRateLimitProcessor _processor; private readonly IHttpContextAccessor accessor; private readonly ILogger logger; - private static SemaphoreSlim ConnectionLimiterSemaphore = new(20); - private static SemaphoreSlim DisconnectLimiterSemaphore = new(20); + private static readonly SemaphoreSlim ConnectionLimiterSemaphore = new(20); + private static readonly SemaphoreSlim DisconnectLimiterSemaphore = new(20); public SignalRLimitFilter( IOptions options, IProcessingStrategy processing, IIpPolicyStore policyStore, IHttpContextAccessor accessor, ILogger logger) diff --git a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj index 55c42ec..78e5a49 100644 --- a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj +++ b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj @@ -10,8 +10,9 @@ - + + @@ -27,7 +28,6 @@ - diff --git a/MareSynchronosServer/MareSynchronosServer/Program.cs b/MareSynchronosServer/MareSynchronosServer/Program.cs index d3c1aed..fa4eaaf 100644 --- a/MareSynchronosServer/MareSynchronosServer/Program.cs +++ b/MareSynchronosServer/MareSynchronosServer/Program.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using MareSynchronosServer.Metrics; using System.Collections.Generic; using MareSynchronosShared.Data; @@ -37,8 +36,6 @@ namespace MareSynchronosServer context.RemoveRange(unfinishedRegistrations); context.RemoveRange(looseFiles); context.SaveChanges(); - - MareMetrics.InitializeMetrics(context, services.GetRequiredService()); } if (args.Length == 0 || args[0] != "dry") diff --git a/MareSynchronosServer/MareSynchronosServer/Startup.cs b/MareSynchronosServer/MareSynchronosServer/Startup.cs index a6b8a1d..32ec88b 100644 --- a/MareSynchronosServer/MareSynchronosServer/Startup.cs +++ b/MareSynchronosServer/MareSynchronosServer/Startup.cs @@ -1,3 +1,4 @@ +using System; using MareSynchronos.API; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -9,15 +10,12 @@ using MareSynchronosServer.Hubs; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR; -using Prometheus; -using Microsoft.Extensions.FileProviders; using Microsoft.AspNetCore.Authorization; -using MareSynchronosServer.Discord; using AspNetCoreRateLimit; -using MareSynchronosServer.Throttling; using Ben.Diagnostics; using MareSynchronosShared.Authentication; using MareSynchronosShared.Data; +using MareSynchronosShared.Protos; namespace MareSynchronosServer { @@ -47,6 +45,15 @@ namespace MareSynchronosServer services.AddSingleton(); services.AddTransient(_ => Configuration); + services.AddGrpcClient(c => + { + c.Address = new Uri(Configuration.GetValue("ServiceAddress")); + }); + services.AddGrpcClient(c => + { + c.Address = new Uri(Configuration.GetValue("ServiceAddress")); + }); + services.AddDbContextPool(options => { options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => @@ -56,15 +63,13 @@ namespace MareSynchronosServer options.EnableThreadSafetyChecks(false); }, Configuration.GetValue("DbContextPoolSize", 1024)); - services.AddHostedService(); services.AddHostedService(provider => provider.GetService()); - services.AddHostedService(); services.AddAuthentication(options => { - options.DefaultScheme = SecretKeyAuthenticationHandler.AuthScheme; + options.DefaultScheme = SecretKeyGrpcAuthenticationHandler.AuthScheme; }) - .AddScheme(SecretKeyAuthenticationHandler.AuthScheme, options => { }); + .AddScheme(SecretKeyGrpcAuthenticationHandler.AuthScheme, options => { }); services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); services.AddSingleton(); @@ -98,27 +103,13 @@ namespace MareSynchronosServer app.UseIpRateLimiting(); - app.UseStaticFiles(); - app.UseHttpLogging(); - app.UseRouting(); - app.UseHttpMetrics(); app.UseWebSockets(); app.UseAuthentication(); app.UseAuthorization(); - var metricServer = new KestrelMetricServer(4980); - metricServer.Start(); - - app.UseStaticFiles(new StaticFileOptions() - { - FileProvider = new PhysicalFileProvider(Configuration["CacheDirectory"]), - RequestPath = "/cache", - ServeUnknownFileTypes = true - }); - app.UseEndpoints(endpoints => { endpoints.MapHub(Api.Path, options => diff --git a/MareSynchronosServer/MareSynchronosServer/SystemInfoService.cs b/MareSynchronosServer/MareSynchronosServer/SystemInfoService.cs index 9c9ad62..c8b8e85 100644 --- a/MareSynchronosServer/MareSynchronosServer/SystemInfoService.cs +++ b/MareSynchronosServer/MareSynchronosServer/SystemInfoService.cs @@ -4,8 +4,9 @@ using System.Threading; using System.Threading.Tasks; using MareSynchronos.API; using MareSynchronosServer.Hubs; -using MareSynchronosServer.Metrics; using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Protos; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -41,11 +42,15 @@ public class SystemInfoService : IHostedService, IDisposable { ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads); _logger.LogInformation("ThreadPool: {workerThreads} workers available, {ioThreads} IO workers available", workerThreads, ioThreads); - MareMetrics.AvailableWorkerThreads.Set(workerThreads); - MareMetrics.AvailableIOWorkerThreads.Set(ioThreads); using var scope = _services.CreateScope(); - using var db = scope.ServiceProvider.GetService(); + using var db = scope.ServiceProvider.GetService()!; + + var metricsServiceClient = scope.ServiceProvider.GetService()!; + _ = metricsServiceClient.SetGauge(new SetGaugeRequest() + { GaugeName = MetricsAPI.GaugeAvailableWorkerThreads, Value = workerThreads }); + _ = metricsServiceClient.SetGauge(new SetGaugeRequest() + { GaugeName = MetricsAPI.GaugeAvailableIOWorkerThreads, Value = ioThreads }); var users = db.Users.Count(c => c.CharacterIdentification != null); diff --git a/MareSynchronosServer/MareSynchronosServer/appsettings.json b/MareSynchronosServer/MareSynchronosServer/appsettings.json index 966a76f..9831f47 100644 --- a/MareSynchronosServer/MareSynchronosServer/appsettings.json +++ b/MareSynchronosServer/MareSynchronosServer/appsettings.json @@ -25,14 +25,9 @@ }, "DbContextPoolSize": 2000, "CdnFullUrl": "https:///cache/", - "FailedAuthForTempBan": 5, - "TempBanDurationInMinutes": 30, - "DiscordBotToken": "", - "UnusedFileRetentionPeriodInDays": 7, - "PurgeUnusedAccounts": true, - "PurgeUnusedAccountsPeriodInDays": 14, "CacheDirectory": "G:\\ServerTest", // do not delete this key and set it to the path where the files will be stored - "CacheSizeHardLimitInGiB": -1, + "ServicesUrl": "http://localhost:5002", + "AllowedHosts": "*", "Kestrel": { "Endpoints": { diff --git a/MareSynchronosServer/MareSynchronosServices/Authentication/FailedAuthorization.cs b/MareSynchronosServer/MareSynchronosServices/Authentication/FailedAuthorization.cs new file mode 100644 index 0000000..3521102 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Authentication/FailedAuthorization.cs @@ -0,0 +1,24 @@ +namespace MareSynchronosServices.Authentication; + +public class FailedAuthorization : IDisposable +{ + private int failedAttempts = 1; + public int FailedAttempts => failedAttempts; + public Task ResetTask { get; set; } + public CancellationTokenSource? ResetCts { get; set; } + + public void Dispose() + { + try + { + ResetCts?.Cancel(); + ResetCts?.Dispose(); + } + catch { } + } + + public void IncreaseFailedAttempts() + { + Interlocked.Increment(ref failedAttempts); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Authentication/SecretKeyAuthenticationHandler.cs b/MareSynchronosServer/MareSynchronosServices/Authentication/SecretKeyAuthenticationHandler.cs new file mode 100644 index 0000000..960eb60 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Authentication/SecretKeyAuthenticationHandler.cs @@ -0,0 +1,165 @@ +using System.Security.Cryptography; +using System.Text; +using MareSynchronosServices.Metrics; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Protos; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosServices.Authentication; + +public class SecretKeyAuthenticationHandler +{ + private readonly ILogger logger; + private readonly MareMetrics metrics; + private const string Unauthorized = "Unauthorized"; + private readonly Dictionary authorizations = new(); + private readonly Dictionary failedAuthorizations = new(); + private readonly object authDictLock = new(); + private readonly object failedAuthLock = new(); + private readonly int failedAttemptsForTempBan; + private readonly int tempBanMinutes; + + public void ClearUnauthorizedUsers() + { + lock (authDictLock) + { + foreach (var item in authorizations.ToArray()) + { + if (item.Value == Unauthorized) + { + authorizations[item.Key] = string.Empty; + } + } + } + } + + public void RemoveAuthentication(string uid) + { + lock (authDictLock) + { + var authorization = authorizations.Where(u => u.Value == uid); + if (authorization.Any()) + { + authorizations.Remove(authorization.First().Key); + } + } + } + + public async Task AuthenticateAsync(MareDbContext mareDbContext, string ip, string secretKey) + { + metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests); + + if (string.IsNullOrEmpty(secretKey)) + { + metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures); + return new AuthReply() { Success = false, Uid = string.Empty }; + } + + lock (failedAuthLock) + { + if (failedAuthorizations.TryGetValue(ip, out var existingFailedAuthorization) && existingFailedAuthorization.FailedAttempts > failedAttemptsForTempBan) + { + metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures); + + existingFailedAuthorization.ResetCts?.Cancel(); + existingFailedAuthorization.ResetCts?.Dispose(); + existingFailedAuthorization.ResetCts = new CancellationTokenSource(); + var token = existingFailedAuthorization.ResetCts.Token; + existingFailedAuthorization.ResetTask = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(tempBanMinutes), token).ConfigureAwait(false); + if (token.IsCancellationRequested) return; + FailedAuthorization? failedAuthorization; + lock (failedAuthLock) + { + failedAuthorizations.Remove(ip, out failedAuthorization); + } + failedAuthorization?.Dispose(); + }, token); + + logger.LogWarning("TempBan {ip} for authorization spam", ip); + return new AuthReply() { Success = false, Uid = string.Empty }; + } + } + + using var sha256 = SHA256.Create(); + var hashedHeader = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(secretKey))).Replace("-", ""); + + string uid; + lock (authDictLock) + { + if (authorizations.TryGetValue(hashedHeader, out uid)) + { + if (uid == Unauthorized) + { + metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures); + + lock (failedAuthLock) + { + logger.LogWarning("Failed authorization from {ip}", ip); + if (failedAuthorizations.TryGetValue(ip, out var auth)) + { + auth.IncreaseFailedAttempts(); + } + else + { + failedAuthorizations[ip] = new FailedAuthorization(); + } + } + + return new AuthReply() { Success = false, Uid = string.Empty }; + } + + metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests); + } + } + + if (string.IsNullOrEmpty(uid)) + { + uid = (await mareDbContext.Auth.AsNoTracking() + .FirstOrDefaultAsync(m => m.HashedKey == hashedHeader).ConfigureAwait(false))?.UserUID; + + if (uid == null) + { + lock (authDictLock) + { + authorizations[hashedHeader] = Unauthorized; + } + + logger.LogWarning("Failed authorization from {ip}", ip); + lock (failedAuthLock) + { + if (failedAuthorizations.TryGetValue(ip, out var auth)) + { + auth.IncreaseFailedAttempts(); + } + else + { + failedAuthorizations[ip] = new FailedAuthorization(); + } + } + + metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures); + return new AuthReply() { Success = false, Uid = string.Empty }; + } + + lock (authDictLock) + { + authorizations[hashedHeader] = uid; + } + } + + metrics.IncCounter(MetricsAPI.CounterAuthenticationSuccesses); + + return new AuthReply() { Success = true, Uid = uid }; + } + + public SecretKeyAuthenticationHandler(IConfiguration configuration, ILogger logger, MareMetrics metrics) + { + this.logger = logger; + this.metrics = metrics; + failedAttemptsForTempBan = configuration.GetValue("FailedAuthForTempBan", 5); + tempBanMinutes = configuration.GetValue("TempBanDurationInMinutes", 30); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/CleanupService.cs b/MareSynchronosServer/MareSynchronosServices/CleanupService.cs similarity index 77% rename from MareSynchronosServer/MareSynchronosServer/CleanupService.cs rename to MareSynchronosServer/MareSynchronosServices/CleanupService.cs index 5eff01d..c64c97b 100644 --- a/MareSynchronosServer/MareSynchronosServer/CleanupService.cs +++ b/MareSynchronosServer/MareSynchronosServices/CleanupService.cs @@ -1,30 +1,26 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MareSynchronosServer.Metrics; -using MareSynchronosShared.Authentication; +using MareSynchronosServices.Authentication; using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; using MareSynchronosShared.Models; +using MareSynchronosShared.Protos; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using MetricsService = MareSynchronosShared.Protos.MetricsService; -namespace MareSynchronosServer +namespace MareSynchronosServices { public class CleanupService : IHostedService, IDisposable { + private readonly MetricsService.MetricsServiceClient _metricsClient; + private readonly SecretKeyAuthenticationHandler _authService; private readonly ILogger _logger; private readonly IServiceProvider _services; private readonly IConfiguration _configuration; private Timer _timer; - public CleanupService(ILogger logger, IServiceProvider services, IConfiguration configuration) + public CleanupService(MetricsService.MetricsServiceClient metricsClient, SecretKeyAuthenticationHandler authService, ILogger logger, IServiceProvider services, IConfiguration configuration) { + _metricsClient = metricsClient; + _authService = authService; _logger = logger; _services = services; _configuration = configuration; @@ -56,9 +52,10 @@ namespace MareSynchronosServer var prevTime = DateTime.Now.Subtract(TimeSpan.FromDays(filesOlderThanDays)); var allFiles = dbContext.Files.ToList(); + var cachedir = _configuration["CacheDirectory"]; foreach (var file in allFiles.Where(f => f.Uploaded)) { - var fileName = Path.Combine(_configuration["CacheDirectory"], file.Hash); + var fileName = Path.Combine(cachedir, file.Hash); var fi = new FileInfo(fileName); if (!fi.Exists) { @@ -67,7 +64,8 @@ namespace MareSynchronosServer } else if (fi.LastAccessTime < prevTime) { - MareMetrics.FilesTotalSize.Dec(fi.Length); + _metricsClient.DecGauge(new GaugeRequest() { GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = fi.Length }); + _metricsClient.DecGauge(new GaugeRequest() { GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1 }); _logger.LogInformation("File outdated: {fileName}", fileName); dbContext.Files.Remove(file); fi.Delete(); @@ -96,8 +94,8 @@ namespace MareSynchronosServer removedHashes.Add(oldestFile.Name.ToLower()); allLocalFiles.Remove(oldestFile); totalCacheSizeInBytes -= oldestFile.Length; - MareMetrics.FilesTotal.Dec(); - MareMetrics.FilesTotalSize.Dec(oldestFile.Length); + _metricsClient.DecGauge(new GaugeRequest() { GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = oldestFile.Length }); + _metricsClient.DecGauge(new GaugeRequest() { GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1 }); oldestFile.Delete(); } @@ -170,14 +168,14 @@ namespace MareSynchronosServer _logger.LogWarning(ex, "Error during user purge"); } - SecretKeyAuthenticationHandler.ClearUnauthorizedUsers(); + _authService.ClearUnauthorizedUsers(); _logger.LogInformation($"Cleanup complete"); dbContext.SaveChanges(); } - public static void PurgeUser(User user, MareDbContext dbContext, IConfiguration _configuration) + public void PurgeUser(User user, MareDbContext dbContext, IConfiguration _configuration) { var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID); @@ -186,7 +184,7 @@ namespace MareSynchronosServer dbContext.Remove(lodestone); } - SecretKeyAuthenticationHandler.RemoveAuthentication(user.UID); + _authService.RemoveAuthentication(user.UID); var auth = dbContext.Auth.Single(a => a.UserUID == user.UID); @@ -196,8 +194,8 @@ namespace MareSynchronosServer var fi = new FileInfo(Path.Combine(_configuration["CacheDirectory"], file.Hash)); if (fi.Exists) { - MareMetrics.FilesTotalSize.Dec(fi.Length); - MareMetrics.FilesTotal.Dec(); + _metricsClient.DecGauge(new GaugeRequest() { GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = fi.Length }); + _metricsClient.DecGauge(new GaugeRequest() { GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1 }); fi.Delete(); } } @@ -210,12 +208,10 @@ namespace MareSynchronosServer var otherPairData = dbContext.ClientPairs.Include(u => u.User) .Where(u => u.OtherUser.UID == user.UID).ToList(); - MareMetrics.Pairs.Dec(ownPairData.Count); - MareMetrics.PairsPaused.Dec(ownPairData.Count(c => c.IsPaused)); - MareMetrics.Pairs.Dec(otherPairData.Count); - MareMetrics.PairsPaused.Dec(otherPairData.Count(c => c.IsPaused)); - MareMetrics.UsersRegistered.Dec(); - + _metricsClient.DecGauge(new GaugeRequest() { GaugeName = MetricsAPI.GaugePairs, Value = ownPairData.Count + otherPairData.Count }); + _metricsClient.DecGauge(new GaugeRequest() { GaugeName = MetricsAPI.GaugePairsPaused, Value = ownPairData.Count(c => c.IsPaused) }); + _metricsClient.DecGauge(new GaugeRequest() { GaugeName = MetricsAPI.GaugeUsersRegistered, Value = 1 }); + dbContext.RemoveRange(otherPairData); dbContext.Remove(auth); dbContext.Remove(user); diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs new file mode 100644 index 0000000..3fdc054 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs @@ -0,0 +1,443 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using Discord; +using Discord.WebSocket; +using MareSynchronosServices.Metrics; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Models; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosServices.Discord; + +public class DiscordBot : IHostedService +{ + private readonly CleanupService cleanupService; + private readonly MareMetrics metrics; + private readonly IServiceProvider services; + private readonly IConfiguration configuration; + private readonly ILogger logger; + private readonly Random random; + private string authToken = string.Empty; + DiscordSocketClient discordClient; + ConcurrentDictionary DiscordLodestoneMapping = new(); + private CancellationTokenSource verificationTaskCts; + private CancellationTokenSource updateStatusCts; + private readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; + private readonly ConcurrentQueue verificationQueue = new(); + + private SemaphoreSlim semaphore; + + public DiscordBot(CleanupService cleanupService, MareMetrics metrics, IServiceProvider services, IConfiguration configuration, ILogger logger) + { + this.cleanupService = cleanupService; + this.metrics = metrics; + this.services = services; + this.configuration = configuration; + this.logger = logger; + this.verificationQueue = new ConcurrentQueue(); + this.semaphore = new SemaphoreSlim(1); + + random = new(); + authToken = configuration.GetValue("DiscordBotToken"); + + discordClient = new(new DiscordSocketConfig() + { + DefaultRetryMode = RetryMode.AlwaysRetry + }); + + discordClient.Log += Log; + } + + private async Task DiscordClient_SlashCommandExecuted(SocketSlashCommand arg) + { + await semaphore.WaitAsync().ConfigureAwait(false); + try + { + if (arg.Data.Name == "register") + { + if (arg.Data.Options.FirstOrDefault(f => f.Name == "overwrite_old_account") != null) + { + await DeletePreviousUserAccount(arg.User.Id).ConfigureAwait(false); + } + + var modal = new ModalBuilder(); + modal.WithTitle("Verify with Lodestone"); + modal.WithCustomId("register_modal"); + modal.AddTextInput("Enter the Lodestone URL of your Character", "lodestoneurl", TextInputStyle.Short, "https://*.finalfantasyxiv.com/lodestone/character//", required: true); + await arg.RespondWithModalAsync(modal.Build()).ConfigureAwait(false); + } + else if (arg.Data.Name == "verify") + { + EmbedBuilder eb = new(); + if (verificationQueue.Any(u => u.User.Id == arg.User.Id)) + { + eb.WithTitle("Already queued for verfication"); + eb.WithDescription("You are already queued for verification. Please wait."); + await arg.RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + else if (!DiscordLodestoneMapping.ContainsKey(arg.User.Id)) + { + eb.WithTitle("Cannot verify registration"); + eb.WithDescription("You need to **/register** first before you can **/verify**"); + await arg.RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + else + { + await arg.DeferAsync(ephemeral: true).ConfigureAwait(false); + verificationQueue.Enqueue(arg); + } + } + else + { + await arg.RespondAsync("idk what you did to get here to start, just follow the instructions as provided.", ephemeral: true).ConfigureAwait(false); + } + } + finally + { + semaphore.Release(); + } + } + + private async Task DeletePreviousUserAccount(ulong id) + { + using var scope = services.CreateScope(); + using var db = scope.ServiceProvider.GetService(); + var discordAuthedUser = await db.LodeStoneAuth.Include(u => u.User).FirstOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false); + if (discordAuthedUser != null) + { + if (discordAuthedUser.User != null) + { + cleanupService.PurgeUser(discordAuthedUser.User, db, configuration); + } + else + { + db.Remove(discordAuthedUser); + } + + await db.SaveChangesAsync().ConfigureAwait(false); + } + } + + private async Task DiscordClient_ModalSubmitted(SocketModal arg) + { + if (arg.Data.CustomId == "register_modal") + { + var embed = await HandleRegisterModalAsync(arg).ConfigureAwait(false); + await arg.RespondAsync(embeds: new Embed[] { embed }, ephemeral: true).ConfigureAwait(false); + } + } + + private async Task HandleVerifyAsync(ulong id) + { + var embedBuilder = new EmbedBuilder(); + + using var scope = services.CreateScope(); + var req = new HttpClient(); + using var db = scope.ServiceProvider.GetService(); + + var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == id); + if (lodestoneAuth != null && DiscordLodestoneMapping.ContainsKey(id)) + { + var randomServer = LodestoneServers[random.Next(LodestoneServers.Length)]; + var response = await req.GetAsync($"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{DiscordLodestoneMapping[id]}").ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (content.Contains(lodestoneAuth.LodestoneAuthString)) + { + DiscordLodestoneMapping.TryRemove(id, out _); + + using var sha256 = SHA256.Create(); + var user = new User(); + + var hasValidUid = false; + while (!hasValidUid) + { + var uid = GenerateRandomString(10); + if (db.Users.Any(u => u.UID == uid)) continue; + user.UID = uid; + hasValidUid = true; + } + + // make the first registered user on the service to admin + if (!await db.Users.AnyAsync().ConfigureAwait(false)) + { + user.IsAdmin = true; + } + + if (configuration.GetValue("PurgeUnusedAccounts")) + { + var purgedDays = configuration.GetValue("PurgeUnusedAccountsPeriodInDays"); + user.LastLoggedIn = DateTime.UtcNow - TimeSpan.FromDays(purgedDays) + TimeSpan.FromDays(1); + } + + var computedHash = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(GenerateRandomString(64)))).Replace("-", ""); + var auth = new Auth() + { + HashedKey = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(computedHash))) + .Replace("-", ""), + User = user, + }; + + await db.Users.AddAsync(user).ConfigureAwait(false); + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + logger.LogInformation("User registered: {userUID}", user.UID); + + metrics.IncGaugeBy(MetricsAPI.GaugeUsersRegistered, 1); + + lodestoneAuth.StartedAt = null; + lodestoneAuth.User = user; + lodestoneAuth.LodestoneAuthString = null; + + embedBuilder.WithTitle("Registration successful"); + embedBuilder.WithDescription("This is your private secret key. Do not share this private secret key with anyone. **If you lose it, it is irrevocably lost.**" + + Environment.NewLine + Environment.NewLine + + $"**{computedHash}**" + + Environment.NewLine + Environment.NewLine + + "Enter this key in Mare Synchronos and hit save to connect to the service." + + Environment.NewLine + + "You should connect as soon as possible to not get caught by the automatic cleanup process." + + Environment.NewLine + + "Have fun."); + } + else + { + embedBuilder.WithTitle("Failed to verify your character"); + embedBuilder.WithDescription("Did not find requested authentication key on your profile. Make sure you have saved *twice*, then do **/verify** again."); + lodestoneAuth.StartedAt = DateTime.UtcNow; + } + } + + await db.SaveChangesAsync().ConfigureAwait(false); + } + else + { + embedBuilder.WithTitle("Your auth has expired or something else went wrong"); + embedBuilder.WithDescription("Start again with **/register**"); + DiscordLodestoneMapping.TryRemove(id, out _); + } + + return embedBuilder.Build(); + } + + private async Task HandleRegisterModalAsync(SocketModal arg) + { + var embed = new EmbedBuilder(); + + var lodestoneId = ParseCharacterIdFromLodestoneUrl(arg.Data.Components.Single(c => c.CustomId == "lodestoneurl").Value); + if (lodestoneId == null) + { + embed.WithTitle("Invalid Lodestone URL"); + embed.WithDescription("The lodestone URL was not valid. It should have following format:" + Environment.NewLine + + "https://eu.finalfantasyxiv.com/lodestone/character/YOUR_LODESTONE_ID/"); + } + else + { + // check if userid is already in db + using var scope = services.CreateScope(); + using var sha256 = SHA256.Create(); + + var hashedLodestoneId = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(lodestoneId.ToString()))).Replace("-", ""); + + using var db = scope.ServiceProvider.GetService(); + + // check if discord id or lodestone id is banned + if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == arg.User.Id.ToString() || a.DiscordIdOrLodestoneAuth == hashedLodestoneId)) + { + embed.WithTitle("no"); + embed.WithDescription("your account is banned"); + } + else if (db.LodeStoneAuth.Any(a => a.DiscordId == arg.User.Id)) + { + // user already in db + embed.WithTitle("Registration failed"); + embed.WithDescription("You cannot register more than one lodestone character to your discord account."); + } + else if (db.LodeStoneAuth.Any(a => a.HashedLodestoneId == hashedLodestoneId)) + { + // character already in db + embed.WithTitle("Registration failed"); + embed.WithDescription("This lodestone character already exists in the Database. If you are the rightful owner for this character and lost your secret key generated with it, contact the developer."); + } + else + { + string lodestoneAuth = await GenerateLodestoneAuth(arg.User.Id, hashedLodestoneId, db).ConfigureAwait(false); + // check if lodestone id is already in db + embed.WithTitle("Authorize your character"); + embed.WithDescription("Add following key to your character profile at https://na.finalfantasyxiv.com/lodestone/my/setting/profile/" + + Environment.NewLine + Environment.NewLine + + $"**{lodestoneAuth}**" + + Environment.NewLine + Environment.NewLine + + $"**! THIS IS NOT THE KEY YOU HAVE TO ENTER IN MARE !**" + + Environment.NewLine + Environment.NewLine + + "Once added and saved, use command **/verify** to finish registration and receive a secret key to use for Mare Synchronos." + + Environment.NewLine + + "You can delete the entry from your profile after verification." + + Environment.NewLine + Environment.NewLine + + "The verification will expire in approximately 15 minutes. If you fail to **/verify** the registration will be invalidated and you have to **/register** again."); + DiscordLodestoneMapping[arg.User.Id] = lodestoneId.ToString(); + } + } + + return embed.Build(); + } + + private async Task GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, MareDbContext dbContext) + { + var auth = GenerateRandomString(32); + LodeStoneAuth lsAuth = new LodeStoneAuth() + { + DiscordId = discordid, + HashedLodestoneId = hashedLodestoneId, + LodestoneAuthString = auth, + StartedAt = DateTime.UtcNow + }; + + dbContext.Add(lsAuth); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + return auth; + } + + private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl) + { + var regex = new Regex(@"https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/\d+"); + var matches = regex.Match(lodestoneUrl); + var isLodestoneUrl = matches.Success; + if (!isLodestoneUrl || matches.Groups.Count < 1) return null; + + lodestoneUrl = matches.Groups[0].ToString(); + var stringId = lodestoneUrl.Split('/', StringSplitOptions.RemoveEmptyEntries).Last(); + if (!int.TryParse(stringId, out int lodestoneId)) + { + return null; + } + + return lodestoneId; + } + + private async Task DiscordClient_Ready() + { + var register = new SlashCommandBuilder() + .WithName("register") + .WithDescription("Registration for the Mare Synchronos server of this Discord") + .AddOption(new SlashCommandOptionBuilder() + .WithName("new_account") + .WithDescription("Starts the registration process for the Mare Synchronos server of this Discord") + .WithType(ApplicationCommandOptionType.SubCommand)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("overwrite_old_account") + .WithDescription("Will forcefully overwrite your current character on the service, if present") + .WithType(ApplicationCommandOptionType.SubCommand)); + + var verify = new SlashCommandBuilder(); + verify.WithName("verify"); + verify.WithDescription("Finishes the registration process for the Mare Synchronos server of this Discord"); + + try + { + await discordClient.CreateGlobalApplicationCommandAsync(register.Build()).ConfigureAwait(false); + await discordClient.CreateGlobalApplicationCommandAsync(verify.Build()).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create command"); + } + } + + private Task Log(LogMessage msg) + { + logger.LogInformation("{msg}", msg); + + return Task.CompletedTask; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + if (!string.IsNullOrEmpty(authToken)) + { + authToken = configuration.GetValue("DiscordBotToken"); + + await discordClient.LoginAsync(TokenType.Bot, authToken).ConfigureAwait(false); + await discordClient.StartAsync().ConfigureAwait(false); + + discordClient.Ready += DiscordClient_Ready; + discordClient.SlashCommandExecuted += DiscordClient_SlashCommandExecuted; + discordClient.ModalSubmitted += DiscordClient_ModalSubmitted; + + _ = ProcessQueueWork(); + _ = UpdateStatusAsync(); + } + } + + private async Task ProcessQueueWork() + { + verificationTaskCts = new CancellationTokenSource(); + while (!verificationTaskCts.IsCancellationRequested) + { + if (verificationQueue.TryDequeue(out var queueitem)) + { + try + { + var dataEmbed = await HandleVerifyAsync(queueitem.User.Id).ConfigureAwait(false); + await queueitem.FollowupAsync(embed: dataEmbed, ephemeral: true).ConfigureAwait(false); + + logger.LogInformation("Sent login information to user"); + } + catch (Exception e) + { + logger.LogError(e, "Error during queue work"); + } + + } + await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).ConfigureAwait(false); + } + } + + private async Task UpdateStatusAsync() + { + updateStatusCts = new(); + while (!updateStatusCts.IsCancellationRequested) + { + await using var scope = services.CreateAsyncScope(); + await using var db = scope.ServiceProvider.GetService(); + + var users = db.Users.Count(c => c.CharacterIdentification != null); + + await discordClient.SetActivityAsync(new Game("Mare for " + users + " Users")).ConfigureAwait(false); + + await Task.Delay(TimeSpan.FromSeconds(15)).ConfigureAwait(false); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + verificationTaskCts?.Cancel(); + updateStatusCts?.Cancel(); + + await discordClient.LogoutAsync().ConfigureAwait(false); + await discordClient.StopAsync().ConfigureAwait(false); + } + + public static string GenerateRandomString(int length, string allowableChars = null) + { + if (string.IsNullOrEmpty(allowableChars)) + allowableChars = @"ABCDEFGHJKLMNPQRSTUVWXYZ0123456789"; + + // Generate random data + var rnd = RandomNumberGenerator.GetBytes(length); + + // Generate the output string + var allowable = allowableChars.ToCharArray(); + var l = allowable.Length; + var chars = new char[length]; + for (var i = 0; i < length; i++) + chars[i] = allowable[rnd[i] % l]; + + return new string(chars); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj b/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj new file mode 100644 index 0000000..7cddfee --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/MareSynchronosServer/MareSynchronosServices/Metrics/MareMetrics.cs b/MareSynchronosServer/MareSynchronosServices/Metrics/MareMetrics.cs new file mode 100644 index 0000000..85d0cce --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Metrics/MareMetrics.cs @@ -0,0 +1,82 @@ +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; + +using Prometheus; + +namespace MareSynchronosServices.Metrics; + +public class MareMetrics +{ + public MareMetrics(IServiceProvider services, IConfiguration configuration) + { + using var scope = services.CreateScope(); + using var dbContext = scope.ServiceProvider.GetService(); + + gauges[MetricsAPI.GaugeUsersRegistered].IncTo(dbContext.Users.Count()); + gauges[MetricsAPI.GaugePairs].IncTo(dbContext.ClientPairs.Count()); + gauges[MetricsAPI.GaugePairsPaused].IncTo(dbContext.ClientPairs.Count(p => p.IsPaused)); + gauges[MetricsAPI.GaugeFilesTotal].IncTo(dbContext.Files.Count()); + gauges[MetricsAPI.GaugeFilesTotalSize].IncTo(Directory.EnumerateFiles(configuration["CacheDirectory"]).Sum(f => new FileInfo(f).Length)); + } + + private readonly Dictionary counters = new() + { + { MetricsAPI.CounterInitializedConnections, Prometheus.Metrics.CreateCounter(MetricsAPI.CounterInitializedConnections, "Initialized Connections") }, + { MetricsAPI.CounterUserPushData, Prometheus.Metrics.CreateCounter(MetricsAPI.CounterUserPushData, "Users pushing data") }, + { MetricsAPI.CounterUserPushDataTo, Prometheus.Metrics.CreateCounter(MetricsAPI.CounterUserPushDataTo, "Users Receiving Data") }, + { MetricsAPI.CounterAuthenticationRequests, Prometheus.Metrics.CreateCounter(MetricsAPI.CounterAuthenticationRequests, "Authentication Requests") }, + { MetricsAPI.CounterAuthenticationCacheHits, Prometheus.Metrics.CreateCounter(MetricsAPI.CounterAuthenticationCacheHits, "Authentication Requests Cache Hits") }, + { MetricsAPI.CounterAuthenticationFailures, Prometheus.Metrics.CreateCounter(MetricsAPI.CounterAuthenticationFailures, "Authentication Requests Failed") }, + { MetricsAPI.CounterAuthenticationSuccesses, Prometheus.Metrics.CreateCounter(MetricsAPI.CounterAuthenticationSuccesses, "Authentication Requests Success") }, + }; + + private readonly Dictionary gauges = new() + { + { MetricsAPI.GaugeConnections, Prometheus.Metrics.CreateGauge(MetricsAPI.GaugeConnections, "Unauthorized Connections") }, + { MetricsAPI.GaugeAuthorizedConnections, Prometheus.Metrics.CreateGauge(MetricsAPI.GaugeConnections, "Authorized Connections") }, + { MetricsAPI.GaugeAvailableIOWorkerThreads, Prometheus.Metrics.CreateGauge(MetricsAPI.GaugeConnections, "Available Threadpool IO Workers") }, + { MetricsAPI.GaugeAvailableWorkerThreads, Prometheus.Metrics.CreateGauge(MetricsAPI.GaugeConnections, "Aavailable Threadpool Workers") }, + { MetricsAPI.GaugeUsersRegistered, Prometheus.Metrics.CreateGauge(MetricsAPI.GaugeConnections, "Total Registrations") }, + { MetricsAPI.GaugePairs, Prometheus.Metrics.CreateGauge(MetricsAPI.GaugeConnections, "Total Pairs") }, + { MetricsAPI.GaugePairsPaused, Prometheus.Metrics.CreateGauge(MetricsAPI.GaugeConnections, "Total Paused Pairs") }, + { MetricsAPI.GaugeFilesTotal, Prometheus.Metrics.CreateGauge(MetricsAPI.GaugeConnections, "Total uploaded files") }, + { MetricsAPI.GaugeFilesTotalSize, Prometheus.Metrics.CreateGauge(MetricsAPI.GaugeConnections, "Total uploaded files (bytes)") }, + }; + + public void SetGaugeTo(string gaugeName, double value) + { + if (gauges.ContainsKey(gaugeName)) + { + gauges[gaugeName].IncTo(value); + } + } + + public void IncGaugeBy(string gaugeName, double value) + { + if (gauges.ContainsKey(gaugeName)) + { + gauges[gaugeName].Inc(value); + } + } + + public void DecGaugeBy(string gaugeName, double value) + { + if (gauges.ContainsKey(gaugeName)) + { + gauges[gaugeName].Dec(value); + } + } + + public void IncCounter(string counterName) + { + IncCounterBy(counterName, 1); + } + + public void IncCounterBy(string counterName, double value) + { + if (counters.ContainsKey(counterName)) + { + counters[counterName].Inc(value); + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Program.cs b/MareSynchronosServer/MareSynchronosServices/Program.cs new file mode 100644 index 0000000..09607b5 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Program.cs @@ -0,0 +1,25 @@ +using MareSynchronosServices; +using MareSynchronosServices.Metrics; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; + +public class Program +{ + public static void Main(string[] args) + { + var hostBuilder = CreateHostBuilder(args); + var host = hostBuilder.Build(); + + host.Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseConsoleLifetime() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseContentRoot(AppContext.BaseDirectory); + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Properties/launchSettings.json b/MareSynchronosServer/MareSynchronosServices/Properties/launchSettings.json new file mode 100644 index 0000000..ea24f06 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "MareSynchronosServices": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5294;https://localhost:7294", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServices/Services/AuthenticationService.cs b/MareSynchronosServer/MareSynchronosServices/Services/AuthenticationService.cs new file mode 100644 index 0000000..a720c61 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Services/AuthenticationService.cs @@ -0,0 +1,40 @@ +using Grpc.Core; +using MareSynchronosServices.Authentication; +using MareSynchronosShared.Data; +using MareSynchronosShared.Protos; + +namespace MareSynchronosServices.Services +{ + public class AuthenticationService : AuthService.AuthServiceBase + { + private readonly ILogger _logger; + private readonly MareDbContext _dbContext; + private readonly SecretKeyAuthenticationHandler _authHandler; + + public AuthenticationService(ILogger logger, MareDbContext dbContext, SecretKeyAuthenticationHandler authHandler) + { + _logger = logger; + _dbContext = dbContext; + _authHandler = authHandler; + } + + public override async Task Authorize(AuthRequest request, ServerCallContext context) + { + return await _authHandler.AuthenticateAsync(_dbContext, request.Ip, request.SecretKey); + } + + public override Task RemoveAuth(RemoveAuthRequest request, ServerCallContext context) + { + _logger.LogInformation("Removing Authentication for {uid}", request.Uid); + _authHandler.RemoveAuthentication(request.Uid); + return Task.FromResult(new Empty()); + } + + public override Task ClearUnauthorized(Empty request, ServerCallContext context) + { + _logger.LogInformation("Clearing unauthorized users"); + _authHandler.ClearUnauthorizedUsers(); + return Task.FromResult(new Empty()); + } + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Services/MetricsService.cs b/MareSynchronosServer/MareSynchronosServices/Services/MetricsService.cs new file mode 100644 index 0000000..26bae42 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Services/MetricsService.cs @@ -0,0 +1,39 @@ +using Grpc.Core; +using MareSynchronosServices.Metrics; +using MareSynchronosShared.Protos; + +namespace MareSynchronosServices.Services; + +public class MetricsService : MareSynchronosShared.Protos.MetricsService.MetricsServiceBase +{ + private readonly MareMetrics metrics; + + public MetricsService(MareMetrics metrics) + { + this.metrics = metrics; + } + + public override Task IncreaseCounter(IncreaseCounterRequest request, ServerCallContext context) + { + metrics.IncCounterBy(request.CounterName, request.Value); + return Task.FromResult(new Empty()); + } + + public override Task SetGauge(SetGaugeRequest request, ServerCallContext context) + { + metrics.SetGaugeTo(request.GaugeName, request.Value); + return Task.FromResult(new Empty()); + } + + public override Task DecGauge(GaugeRequest request, ServerCallContext context) + { + metrics.DecGaugeBy(request.GaugeName, request.Value); + return Task.FromResult(new Empty()); + } + + public override Task IncGauge(GaugeRequest request, ServerCallContext context) + { + metrics.IncGaugeBy(request.GaugeName, request.Value); + return Task.FromResult(new Empty()); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Startup.cs b/MareSynchronosServer/MareSynchronosServices/Startup.cs new file mode 100644 index 0000000..0095df9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/Startup.cs @@ -0,0 +1,55 @@ +using MareSynchronosServer; +using MareSynchronosServices.Authentication; +using MareSynchronosServices.Discord; +using MareSynchronosServices.Metrics; +using MareSynchronosServices.Services; +using MareSynchronosShared.Authentication; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Prometheus; + +namespace MareSynchronosServices; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddDbContextPool(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, Configuration.GetValue("DbContextPoolSize", 1024)); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(_ => Configuration); + services.AddHostedService(provider => provider.GetService()); + services.AddHostedService(); + services.AddGrpc(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + + var metricServer = new KestrelMetricServer(4980); + metricServer.Start(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + }); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/appsettings.Development.json b/MareSynchronosServer/MareSynchronosServices/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServices/appsettings.json b/MareSynchronosServer/MareSynchronosServices/appsettings.json new file mode 100644 index 0000000..fdc0173 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/appsettings.json @@ -0,0 +1,29 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=mare;Username=postgres" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Protocols": "Http2", + "Url": "http://+:5002" + } + } + }, + "DbContextPoolSize": 1024, + "DiscordBotToken": "", + "UnusedFileRetentionPeriodInDays": 7, + "PurgeUnusedAccounts": true, + "PurgeUnusedAccountsPeriodInDays": 14, + "CacheSizeHardLimitInGiB": -1, + "CacheDirectory": "G:\\ServerTest", // do not delete this key and set it to the path where the files will be stored + "FailedAuthForTempBan": 5, + "TempBanDurationInMinutes": 30, + "AllowedHosts": "*" +} diff --git a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticationHandler.cs b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticationHandler.cs deleted file mode 100644 index 57ab121..0000000 --- a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticationHandler.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using System.Text.Encodings.Web; -using MareSynchronosServer; -using MareSynchronosServer.Metrics; -using MareSynchronosShared.Data; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using ISystemClock = Microsoft.AspNetCore.Authentication.ISystemClock; - -namespace MareSynchronosShared.Authentication -{ - public class FailedAuthorization : IDisposable - { - private int failedAttempts = 1; - public int FailedAttempts => failedAttempts; - public Task ResetTask { get; set; } - public CancellationTokenSource? ResetCts { get; set; } - - public void Dispose() - { - try - { - ResetCts?.Cancel(); - ResetCts?.Dispose(); - } - catch { } - } - - public void IncreaseFailedAttempts() - { - Interlocked.Increment(ref failedAttempts); - } - } - - public class SecretKeyAuthenticationHandler : AuthenticationHandler - { - private readonly IHttpContextAccessor _accessor; - private readonly MareDbContext _mareDbContext; - private readonly IConfiguration _configuration; - public const string AuthScheme = "SecretKeyAuth"; - private const string unauthorized = "Unauthorized"; - public static readonly Dictionary Authentications = new(); - private static readonly Dictionary FailedAuthorizations = new(); - private static readonly object authDictLock = new(); - private static readonly object failedAuthLock = new(); - private readonly int failedAttemptsForTempBan; - private readonly int tempBanMinutes; - - public static void ClearUnauthorizedUsers() - { - lock (authDictLock) - { - foreach (var item in Authentications.ToArray()) - { - if (item.Value == unauthorized) - { - Authentications[item.Key] = string.Empty; - } - } - } - } - - public static void RemoveAuthentication(string uid) - { - lock (authDictLock) - { - var auth = Authentications.Where(u => u.Value == uid); - if (auth.Any()) - { - Authentications.Remove(auth.First().Key); - } - } - } - - protected override async Task HandleAuthenticateAsync() - { - MareMetrics.AuthenticationRequests.Inc(); - - if (!Request.Headers.ContainsKey("Authorization")) - { - MareMetrics.AuthenticationFailures.Inc(); - return AuthenticateResult.Fail("Failed Authorization"); - } - - var authHeader = Request.Headers["Authorization"].ToString(); - - if (string.IsNullOrEmpty(authHeader)) - { - MareMetrics.AuthenticationFailures.Inc(); - return AuthenticateResult.Fail("Failed Authorization"); - } - - var ip = _accessor.GetIpAddress(); - - lock (failedAuthLock) - { - if (FailedAuthorizations.TryGetValue(ip, out var failedAuth) && failedAuth.FailedAttempts > failedAttemptsForTempBan) - { - MareMetrics.AuthenticationFailures.Inc(); - - failedAuth.ResetCts?.Cancel(); - failedAuth.ResetCts?.Dispose(); - failedAuth.ResetCts = new CancellationTokenSource(); - var token = failedAuth.ResetCts.Token; - failedAuth.ResetTask = Task.Run(async () => - { - await Task.Delay(TimeSpan.FromMinutes(tempBanMinutes), token).ConfigureAwait(false); - if (token.IsCancellationRequested) return; - FailedAuthorization fauth; - lock (failedAuthLock) - { - FailedAuthorizations.Remove(ip, out fauth); - } - fauth.Dispose(); - }, token); - - Logger.LogWarning("TempBan {ip} for authorization spam", ip); - return AuthenticateResult.Fail("Failed Authorization"); - } - } - - using var sha256 = SHA256.Create(); - var hashedHeader = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(authHeader))).Replace("-", ""); - - string uid; - lock (authDictLock) - { - if (Authentications.TryGetValue(hashedHeader, out uid)) - { - if (uid == unauthorized) - { - MareMetrics.AuthenticationFailures.Inc(); - - lock (failedAuthLock) - { - Logger.LogWarning("Failed authorization from {ip}", ip); - if (FailedAuthorizations.TryGetValue(ip, out var auth)) - { - auth.IncreaseFailedAttempts(); - } - else - { - FailedAuthorizations[ip] = new FailedAuthorization(); - } - } - - return AuthenticateResult.Fail("Failed Authorization"); - } - - MareMetrics.AuthenticationCacheHits.Inc(); - } - } - - if (string.IsNullOrEmpty(uid)) - { - uid = (await _mareDbContext.Auth.AsNoTracking() - .FirstOrDefaultAsync(m => m.HashedKey == hashedHeader).ConfigureAwait(false))?.UserUID; - - if (uid == null) - { - lock (authDictLock) - { - Authentications[hashedHeader] = unauthorized; - } - - Logger.LogWarning("Failed authorization from {ip}", ip); - lock (failedAuthLock) - { - if (FailedAuthorizations.TryGetValue(ip, out var auth)) - { - auth.IncreaseFailedAttempts(); - } - else - { - FailedAuthorizations[ip] = new FailedAuthorization(); - } - } - - MareMetrics.AuthenticationFailures.Inc(); - return AuthenticateResult.Fail("Failed Authorization"); - } - else - { - Authentications[hashedHeader] = uid; - } - } - - var claims = new List { - new Claim(ClaimTypes.NameIdentifier, uid) - }; - - var identity = new ClaimsIdentity(claims, nameof(SecretKeyAuthenticationHandler)); - var principal = new ClaimsPrincipal(identity); - var ticket = new AuthenticationTicket(principal, Scheme.Name); - - MareMetrics.AuthenticationSuccesses.Inc(); - - return AuthenticateResult.Success(ticket); - } - - public SecretKeyAuthenticationHandler(IOptionsMonitor options, IHttpContextAccessor accessor, - MareDbContext mareDbContext, IConfiguration configuration, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) - { - _accessor = accessor; - _mareDbContext = mareDbContext; - _configuration = configuration; - failedAttemptsForTempBan = _configuration.GetValue("FailedAuthForTempBan", 5); - tempBanMinutes = _configuration.GetValue("TempBanDurationInMinutes", 30); - } - } -} diff --git a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyGrpcAuthenticationHandler.cs b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyGrpcAuthenticationHandler.cs new file mode 100644 index 0000000..6a9ad18 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyGrpcAuthenticationHandler.cs @@ -0,0 +1,59 @@ +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using MareSynchronosServer; +using MareSynchronosShared.Data; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Protos; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ISystemClock = Microsoft.AspNetCore.Authentication.ISystemClock; + +namespace MareSynchronosShared.Authentication +{ + + public class SecretKeyGrpcAuthenticationHandler : AuthenticationHandler + { + public const string AuthScheme = "SecretKeyGrpcAuth"; + + private readonly AuthService.AuthServiceClient _authClient; + private readonly IHttpContextAccessor _accessor; + + public SecretKeyGrpcAuthenticationHandler(IHttpContextAccessor accessor, AuthService.AuthServiceClient authClient, + IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { + this._authClient = authClient; + _accessor = accessor; + } + + protected override async Task HandleAuthenticateAsync() + { + Request.Headers.TryGetValue("Authorization", out var authHeader); + var ip = _accessor.GetIpAddress(); + + var authResult = await _authClient.AuthorizeAsync(new AuthRequest() {Ip = ip, SecretKey = authHeader}); + + if (!authResult.Success) + { + return AuthenticateResult.Fail("Failed Authorization"); + } + + string uid = authResult.Uid; + + var claims = new List + { + new(ClaimTypes.NameIdentifier, uid) + }; + + var identity = new ClaimsIdentity(claims, nameof(SecretKeyGrpcAuthenticationHandler)); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, Scheme.Name); + + return AuthenticateResult.Success(ticket); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj index f81b7f4..afba71f 100644 --- a/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj +++ b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj @@ -6,10 +6,19 @@ enable + + + + + + + + + @@ -22,4 +31,10 @@ + + + Both + + + diff --git a/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs b/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs deleted file mode 100644 index 7cd69d2..0000000 --- a/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.IO; -using System.Linq; -using MareSynchronosShared.Data; -using Microsoft.Extensions.Configuration; -using Prometheus; - -namespace MareSynchronosServer.Metrics -{ - public class MareMetrics - { - public static readonly Counter InitializedConnections = - Prometheus.Metrics.CreateCounter("mare_initialized_connections", "Initialized Connections"); - public static readonly Gauge Connections = - Prometheus.Metrics.CreateGauge("mare_unauthorized_connections", "Unauthorized Connections"); - public static readonly Gauge AuthorizedConnections = - Prometheus.Metrics.CreateGauge("mare_authorized_connections", "Authorized Connections"); - public static readonly Gauge AvailableWorkerThreads = Prometheus.Metrics.CreateGauge("mare_available_threadpool", "Available Threadpool Workers"); - public static readonly Gauge AvailableIOWorkerThreads = Prometheus.Metrics.CreateGauge("mare_available_threadpool_io", "Available Threadpool IO Workers"); - - public static readonly Gauge UsersRegistered = Prometheus.Metrics.CreateGauge("mare_users_registered", "Total Registrations"); - - public static readonly Gauge Pairs = Prometheus.Metrics.CreateGauge("mare_pairs", "Total Pairs"); - public static readonly Gauge PairsPaused = Prometheus.Metrics.CreateGauge("mare_pairs_paused", "Total Paused Pairs"); - - public static readonly Gauge FilesTotal = Prometheus.Metrics.CreateGauge("mare_files", "Total uploaded files"); - public static readonly Gauge FilesTotalSize = - Prometheus.Metrics.CreateGauge("mare_files_size", "Total uploaded files (bytes)"); - - public static readonly Counter UserPushData = Prometheus.Metrics.CreateCounter("mare_user_push", "Users pushing data"); - public static readonly Counter UserPushDataTo = - Prometheus.Metrics.CreateCounter("mare_user_push_to", "Users Receiving Data"); - - public static readonly Counter UserDownloadedFiles = - Prometheus.Metrics.CreateCounter("mare_user_downloaded_files", "Total Downloaded Files by Users"); - public static readonly Counter UserDownloadedFilesSize = - Prometheus.Metrics.CreateCounter("mare_user_downloaded_files_size", "Total Downloaded Files Size by Users"); - - public static readonly Gauge - CPUUsage = Prometheus.Metrics.CreateGauge("mare_cpu_usage", "Total calculated CPU usage in %"); - public static readonly Gauge RAMUsage = - Prometheus.Metrics.CreateGauge("mare_ram_usage", "Total calculated RAM usage in bytes for Mare + MSSQL"); - public static readonly Gauge NetworkOut = Prometheus.Metrics.CreateGauge("mare_network_out", "Network out in byte/s"); - public static readonly Gauge NetworkIn = Prometheus.Metrics.CreateGauge("mare_network_in", "Network in in byte/s"); - public static readonly Counter AuthenticationRequests = Prometheus.Metrics.CreateCounter("mare_auth_requests", "Mare Authentication Requests"); - public static readonly Counter AuthenticationCacheHits = Prometheus.Metrics.CreateCounter("mare_auth_requests_cachehit", "Mare Authentication Requests Cache Hits"); - public static readonly Counter AuthenticationFailures = Prometheus.Metrics.CreateCounter("mare_auth_requests_fail", "Mare Authentication Requests Failed"); - public static readonly Counter AuthenticationSuccesses = Prometheus.Metrics.CreateCounter("mare_auth_requests_success", "Mare Authentication Requests Success"); - - public static void InitializeMetrics(MareDbContext dbContext, IConfiguration configuration) - { - UsersRegistered.IncTo(dbContext.Users.Count()); - Pairs.IncTo(dbContext.ClientPairs.Count()); - PairsPaused.IncTo(dbContext.ClientPairs.Count(p => p.IsPaused)); - FilesTotal.IncTo(dbContext.Files.Count()); - FilesTotalSize.IncTo(Directory.EnumerateFiles(configuration["CacheDirectory"]).Sum(f => new FileInfo(f).Length)); - } - } -} diff --git a/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs b/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs new file mode 100644 index 0000000..1d420e9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs @@ -0,0 +1,21 @@ +namespace MareSynchronosShared.Metrics; + +public class MetricsAPI +{ + public const string CounterInitializedConnections = "mare_initialized_connections"; + public const string GaugeConnections = "mare_unauthorized_connections"; + public const string GaugeAuthorizedConnections = "mare_authorized_connections"; + public const string GaugeAvailableWorkerThreads = "mare_available_threadpool"; + public const string GaugeAvailableIOWorkerThreads = "mare_available_threadpool_io"; + public const string GaugeUsersRegistered = "mare_users_registered"; + public const string GaugePairs = "mare_pairs"; + public const string GaugePairsPaused = "mare_pairs_paused"; + public const string GaugeFilesTotal = "mare_files"; + public const string GaugeFilesTotalSize = "mare_files_size"; + public const string CounterUserPushData = "mare_user_push"; + public const string CounterUserPushDataTo = "mare_user_push_to"; + public const string CounterAuthenticationRequests = "mare_auth_requests"; + public const string CounterAuthenticationCacheHits = "mare_auth_requests_cachehit"; + public const string CounterAuthenticationFailures = "mare_auth_requests_fail"; + public const string CounterAuthenticationSuccesses = "mare_auth_requests_success"; +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Protos/mareservices.proto b/MareSynchronosServer/MareSynchronosShared/Protos/mareservices.proto new file mode 100644 index 0000000..96e405d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Protos/mareservices.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +option csharp_namespace = "MareSynchronosShared.Protos"; + +package mareservices; + +service AuthService { + rpc Authorize (AuthRequest) returns (AuthReply); + rpc RemoveAuth (RemoveAuthRequest) returns (Empty); + rpc ClearUnauthorized (Empty) returns (Empty); +} + +service MetricsService { + rpc IncreaseCounter (IncreaseCounterRequest) returns (Empty); + rpc SetGauge (SetGaugeRequest) returns (Empty); + rpc DecGauge (GaugeRequest) returns (Empty); + rpc IncGauge (GaugeRequest) returns (Empty); +} + +message Empty { } + +message GaugeRequest { + string gaugeName = 1; + double value = 2; +} + +message SetGaugeRequest { + string gaugeName = 1; + double value = 2; +} + +message IncreaseCounterRequest { + string counterName = 1; + double value = 2; +} + +message RemoveAuthRequest { + string uid = 1; +} + +message AuthRequest { + string ip = 1; + string secretKey = 2; +} + +message AuthReply { + bool success = 1; + string uid = 2; +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj b/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj new file mode 100644 index 0000000..31bc086 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Program.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Program.cs new file mode 100644 index 0000000..663da32 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Program.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR; + +namespace MareSynchronosStaticFilesServer; + +public class Program +{ + public static void Main(string[] args) + { + var hostBuilder = CreateHostBuilder(args); + var host = hostBuilder.Build(); + + host.Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseConsoleLifetime() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseContentRoot(AppContext.BaseDirectory); + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Properties/launchSettings.json b/MareSynchronosServer/MareSynchronosStaticFilesServer/Properties/launchSettings.json new file mode 100644 index 0000000..6ba3599 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:21378", + "sslPort": 44331 + } + }, + "profiles": { + "MareSynchronosStaticFilesServer": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7094;http://localhost:5094", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs new file mode 100644 index 0000000..c1f99a9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs @@ -0,0 +1,57 @@ +using MareSynchronosShared.Authentication; +using MareSynchronosShared.Data; +using MareSynchronosShared.Models; +using MareSynchronosShared.Protos; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.FileProviders; + +namespace MareSynchronosStaticFilesServer; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddHttpContextAccessor(); + + services.AddTransient(_ => Configuration); + + services.AddGrpcClient(c => + { + c.Address = new Uri(Configuration.GetValue("ServiceAddress")); + }); + + services.AddAuthentication(options => + { + options.DefaultScheme = SecretKeyGrpcAuthenticationHandler.AuthScheme; + }) + .AddScheme(SecretKeyGrpcAuthenticationHandler.AuthScheme, options => { }); + services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseStaticFiles(); + app.UseHttpLogging(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseStaticFiles(new StaticFileOptions() + { + FileProvider = new PhysicalFileProvider(Configuration["CacheDirectory"]), + RequestPath = "/cache", + ServeUnknownFileTypes = true + }); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.Development.json b/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.Development.json new file mode 100644 index 0000000..770d3e9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.json b/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.json new file mode 100644 index 0000000..7eb3997 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/appsettings.json @@ -0,0 +1,21 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=mare;Username=postgres" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:5001" + } + } + }, + "AllowedHosts": "*", + "CacheDirectory": "G:\\ServerTest", // do not delete this key and set it to the path where the files will be stored + "ServicesUrl": "http://localhost:5002" +}