From 0f697bc5f3dbecdd511aec3d81864098d2bb374c Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Mon, 14 Nov 2022 01:56:45 +0100 Subject: [PATCH 1/5] fixes issue where character hash of paused users in a syncshell was uselessly sent --- .../MareSynchronosServer/Hubs/MareHub.Functions.cs | 10 ++++++---- .../MareSynchronosServer/Hubs/MareHub.Groups.cs | 1 + .../MareSynchronosServer/Utils/PauseState.cs | 4 +++- .../MareSynchronosServer/Utils/PausedEntry.cs | 7 +++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs index 55eda08..6d9a29a 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs @@ -22,7 +22,8 @@ public partial class MareHub { UID = Convert.ToString(userPair.OtherUserUID), GID = "DIRECT", - PauseState = (userPair.IsPaused || otherUserPair.IsPaused) + PauseStateSelf = userPair.IsPaused, + PauseStateOther = otherUserPair.IsPaused }) .Union( (from userGroupPair in _dbContext.GroupPairs @@ -34,15 +35,16 @@ public partial class MareHub { UID = Convert.ToString(otherGroupPair.GroupUserUID), GID = Convert.ToString(otherGroupPair.GroupGID), - PauseState = (userGroupPair.IsPaused || otherGroupPair.IsPaused) + PauseStateSelf = userGroupPair.IsPaused, + PauseStateOther = otherGroupPair.IsPaused, }) ).AsNoTracking().ToListAsync().ConfigureAwait(false); - return query.GroupBy(g => g.UID, g => (g.GID, g.PauseState), + return query.GroupBy(g => g.UID, g => (g.GID, g.PauseStateSelf, g.PauseStateOther), (key, g) => new PausedEntry { UID = key, - PauseStates = g.Select(p => new PauseState() { GID = string.Equals(p.GID, "DIRECT", StringComparison.Ordinal) ? null : p.GID, IsPaused = p.PauseState }) + PauseStates = g.Select(p => new PauseState() { GID = string.Equals(p.GID, "DIRECT", StringComparison.Ordinal) ? null : p.GID, IsSelfPaused = p.PauseStateSelf, IsOtherPaused = p.PauseStateOther }) .ToList() }, StringComparer.Ordinal).ToList(); } diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs index ba1f471..47bde0c 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs @@ -407,6 +407,7 @@ public partial class MareHub { if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) continue; if (userPair.IsPausedExcludingGroup(gid) is PauseInfo.Unpaused) continue; + if (userPair.IsOtherPausedForSpecificGroup(gid) is PauseInfo.Paused) continue; } var groupUserIdent = _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID); diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs b/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs index 8106f9f..f656c3a 100644 --- a/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs +++ b/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs @@ -3,5 +3,7 @@ public record PauseState { public string GID { get; set; } - public bool IsPaused { get; set; } + public bool IsPaused => IsSelfPaused || IsOtherPaused; + public bool IsSelfPaused { get; set; } + public bool IsOtherPaused { get; set; } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs b/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs index 00fe0ae..21187f4 100644 --- a/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs +++ b/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs @@ -37,6 +37,13 @@ public record PausedEntry } } + public PauseInfo IsOtherPausedForSpecificGroup(string gid) + { + var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal)); + if (state == null) return PauseInfo.NoConnection; + return state.IsOtherPaused ? PauseInfo.Paused : PauseInfo.Unpaused; + } + public PauseInfo IsPausedForSpecificGroup(string gid) { var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal)); From 3a84afc579e0043a83f15f103f879ee86ba2e2d9 Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Wed, 16 Nov 2022 23:15:37 +0100 Subject: [PATCH 2/5] add /relink command to discord, refactor verification --- .../Discord/DiscordBotServices.cs | 157 +--------- .../Discord/MareModule.cs | 294 +++++++++++++++++- 2 files changed, 297 insertions(+), 154 deletions(-) diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs index 6faec76..59ec3d3 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs @@ -1,44 +1,34 @@ -using Discord; -using MareSynchronosShared.Data; -using System; +using System; using System.Threading.Tasks; +using System.Collections.Generic; using System.Collections.Concurrent; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.EntityFrameworkCore; -using Discord.WebSocket; -using System.Linq; using MareSynchronosShared.Metrics; -using MareSynchronosShared.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using System.Net.Http; -using System.Text; -using System.Security.Cryptography; using System.Threading; namespace MareSynchronosServices.Discord; public class DiscordBotServices { - public readonly ConcurrentQueue verificationQueue = new(); + public readonly ConcurrentQueue> verificationQueue = new(); public ConcurrentDictionary LastVanityChange = new(); public ConcurrentDictionary LastVanityGidChange = new(); public ConcurrentDictionary DiscordLodestoneMapping = new(); - private readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; - private readonly IServiceProvider _services; - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly MareMetrics _metrics; - private readonly Random random; + public ConcurrentDictionary DiscordRelinkLodestoneMapping = new(); + public readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; + public IConfiguration Configuration { get; init; } + public ILogger Logger { get; init; } + public MareMetrics Metrics { get; init; } + public Random Random { get; init; } private CancellationTokenSource? verificationTaskCts; - public DiscordBotServices(IServiceProvider services, IConfiguration configuration, ILogger logger, MareMetrics metrics) + public DiscordBotServices(IConfiguration configuration, ILogger logger, MareMetrics metrics) { - _services = services; - _configuration = configuration.GetRequiredSection("MareSynchronos"); - _logger = logger; - _metrics = metrics; - random = new(); + Configuration = configuration.GetRequiredSection("MareSynchronos"); + Logger = logger; + Metrics = metrics; + Random = new(); } public async Task Start() @@ -60,130 +50,17 @@ public class DiscordBotServices { try { - var dataEmbed = await HandleVerifyAsync(queueitem.User.Id).ConfigureAwait(false); - await queueitem.FollowupAsync(embed: dataEmbed, ephemeral: true).ConfigureAwait(false); + queueitem.Value.Invoke(); - _logger.LogInformation("Sent login information to user"); + Logger.LogInformation("Sent login information to user"); } catch (Exception e) { - _logger.LogError(e, "Error during queue work"); + Logger.LogError(e, "Error during queue work"); } } await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).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); - } - - 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 || u.Alias == 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) + DateTime.UtcNow.ToString()))).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.IncGauge(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(); - } } diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs index 9e99d34..fa254e0 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs @@ -11,9 +11,13 @@ using System.Linq; using Prometheus; using MareSynchronosServices.Authentication; using MareSynchronosShared.Models; -using System.Text; -using System.Security.Cryptography; using MareSynchronosServices.Identity; +using MareSynchronosShared.Metrics; +using Microsoft.Extensions.Configuration; +using System.Net.Http; +using MareSynchronosShared.Utils; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; namespace MareSynchronosServices.Discord; @@ -78,7 +82,7 @@ public class MareModule : InteractionModuleBase public async Task Verify() { EmbedBuilder eb = new(); - if (_botServices.verificationQueue.Any(u => u.User.Id == Context.User.Id)) + if (_botServices.verificationQueue.Any(u => u.Key == Context.User.Id)) { eb.WithTitle("Already queued for verfication"); eb.WithDescription("You are already queued for verification. Please wait."); @@ -93,7 +97,30 @@ public class MareModule : InteractionModuleBase else { await DeferAsync(ephemeral: true).ConfigureAwait(false); - _botServices.verificationQueue.Enqueue((SocketSlashCommand)Context.Interaction); + _botServices.verificationQueue.Enqueue(new KeyValuePair(Context.User.Id, async () => await HandleVerifyAsync((SocketSlashCommand)Context.Interaction))); + } + } + + [SlashCommand("verify_relink", "Finishes the relink process for your user on the Mare Synchronos server of this Discord")] + public async Task VerifyRelink() + { + EmbedBuilder eb = new(); + if (_botServices.verificationQueue.Any(u => u.Key == Context.User.Id)) + { + eb.WithTitle("Already queued for verfication"); + eb.WithDescription("You are already queued for verification. Please wait."); + await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + else if (!_botServices.DiscordLodestoneMapping.ContainsKey(Context.User.Id)) + { + eb.WithTitle("Cannot verify relink"); + eb.WithDescription("You need to **/relink** first before you can **/verify_relink**"); + await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + } + else + { + await DeferAsync(ephemeral: true).ConfigureAwait(false); + _botServices.verificationQueue.Enqueue(new KeyValuePair(Context.User.Id, async () => await HandleVerifyRelinkAsync((SocketSlashCommand)Context.Interaction))); } } @@ -115,6 +142,12 @@ public class MareModule : InteractionModuleBase await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); } + [SlashCommand("relink", "Allows you to link a new Discord account to an existing Mare account")] + public async Task Relink() + { + await RespondWithModalAsync("relink_modal").ConfigureAwait(false); + } + [ModalInteraction("recover_modal")] public async Task RecoverModal(LodestoneModal modal) { @@ -129,6 +162,13 @@ public class MareModule : InteractionModuleBase await RespondAsync(embeds: new Embed[] { embed }, ephemeral: true).ConfigureAwait(false); } + [ModalInteraction("relink_modal")] + public async Task RelinkModal(LodestoneModal modal) + { + var embed = await HandleRelinkModalAsync(modal, Context.User.Id).ConfigureAwait(false); + await RespondAsync(embeds: new Embed[] { embed }, ephemeral: true).ConfigureAwait(false); + } + private async Task HandleUserInfo(EmbedBuilder eb, ulong id, ulong? optionalUser = null, string? uid = null) { using var scope = _services.CreateScope(); @@ -226,9 +266,8 @@ public class MareModule : InteractionModuleBase else { using var scope = _services.CreateScope(); - using var sha256 = SHA256.Create(); - var hashedLodestoneId = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(lodestoneId.ToString()))).Replace("-", ""); + var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); await using var db = scope.ServiceProvider.GetService(); var existingLodestoneAuth = await db.LodeStoneAuth.Include("User") @@ -249,11 +288,10 @@ public class MareModule : InteractionModuleBase db.Auth.Remove(previousAuth); } - var computedHash = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(DiscordBotServices.GenerateRandomString(64) + DateTime.UtcNow.ToString()))).Replace("-", ""); + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); var auth = new Auth() { - HashedKey = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(computedHash))) - .Replace("-", ""), + HashedKey = StringUtils.Sha256String(computedHash), User = existingLodestoneAuth.User, }; @@ -290,9 +328,8 @@ public class MareModule : InteractionModuleBase { // 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("-", ""); + var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); using var db = scope.ServiceProvider.GetService(); @@ -312,7 +349,7 @@ public class MareModule : InteractionModuleBase { // 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."); + embed.WithDescription("This lodestone character already exists in the Database. If you want to attach this character to your current Discord account use **/relink**."); } else { @@ -327,7 +364,7 @@ public class MareModule : InteractionModuleBase + 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." + + "__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."); _botServices.DiscordLodestoneMapping[userid] = lodestoneId.ToString(); @@ -337,9 +374,70 @@ public class MareModule : InteractionModuleBase return embed.Build(); } + private async Task HandleRelinkModalAsync(LodestoneModal arg, ulong userid) + { + var embed = new EmbedBuilder(); + + var lodestoneId = ParseCharacterIdFromLodestoneUrl(arg.LodestoneUrl); + 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(); + + var hashedLodestoneId = StringUtils.Sha256String(lodestoneId.ToString()); + + using var db = scope.ServiceProvider.GetService(); + + // check if discord id or lodestone id is banned + if (db.BannedRegistrations.Any(a => a.DiscordIdOrLodestoneAuth == userid.ToString() || a.DiscordIdOrLodestoneAuth == hashedLodestoneId)) + { + embed.WithTitle("no"); + embed.WithDescription("your account is banned"); + } + else if (db.LodeStoneAuth.Any(a => a.DiscordId == userid)) + { + // user already in db + embed.WithTitle("Relink 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("Relink failed"); + embed.WithDescription("This lodestone character does not exist in the database."); + } + else + { + string lodestoneAuth = await GenerateLodestoneAuth(userid, hashedLodestoneId, db).ConfigureAwait(false); + // check if lodestone id is already in db + embed.WithTitle("Authorize your character for relinking"); + 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_relink** to finish relink and receive a new 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_relink** the relink will be invalidated and you have to **/relink** again."); + _botServices.DiscordRelinkLodestoneMapping[userid] = lodestoneId.ToString(); + } + } + + return embed.Build(); + } + private async Task GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, MareDbContext dbContext) { - var auth = DiscordBotServices.GenerateRandomString(32); + var auth = StringUtils.GenerateRandomString(32); LodeStoneAuth lsAuth = new LodeStoneAuth() { DiscordId = discordid, @@ -509,4 +607,172 @@ public class MareModule : InteractionModuleBase await db.SaveChangesAsync().ConfigureAwait(false); } } + + private async Task HandleVerifyRelinkAsync(SocketSlashCommand cmd) + { + 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 == cmd.User.Id); + if (lodestoneAuth != null && _botServices.DiscordRelinkLodestoneMapping.ContainsKey(cmd.User.Id)) + { + var randomServer = _botServices.LodestoneServers[_botServices.Random.Next(_botServices.LodestoneServers.Length)]; + var response = await req.GetAsync($"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{_botServices.DiscordRelinkLodestoneMapping[cmd.User.Id]}").ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (content.Contains(lodestoneAuth.LodestoneAuthString)) + { + _botServices.DiscordRelinkLodestoneMapping.TryRemove(cmd.User.Id, out _); + + var existingLodestoneAuth = db.LodeStoneAuth.Include(u => u.User).SingleOrDefault(u => u.DiscordId != cmd.User.Id && u.HashedLodestoneId == lodestoneAuth.HashedLodestoneId); + + var previousAuth = await db.Auth.FirstOrDefaultAsync(u => u.UserUID == existingLodestoneAuth.User.UID); + if (previousAuth != null) + { + db.Auth.Remove(previousAuth); + } + + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + var auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = existingLodestoneAuth.User, + }; + + lodestoneAuth.StartedAt = null; + lodestoneAuth.LodestoneAuthString = null; + lodestoneAuth.User = existingLodestoneAuth.User; + + db.LodeStoneAuth.Remove(existingLodestoneAuth); + + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + _botServices.Logger.LogInformation("User relinked: {userUID}", lodestoneAuth.User.UID); + + embedBuilder.WithTitle("Relink successful"); + embedBuilder.WithDescription("This is your **new** 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."); + } + 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 **/relink_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 **/relink**"); + _botServices.DiscordLodestoneMapping.TryRemove(cmd.User.Id, out _); + } + + var dataEmbed = embedBuilder.Build(); + + await cmd.FollowupAsync(embed: dataEmbed, ephemeral: true).ConfigureAwait(false); + } + + private async Task HandleVerifyAsync(SocketSlashCommand cmd) + { + 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 == cmd.User.Id); + if (lodestoneAuth != null && _botServices.DiscordLodestoneMapping.ContainsKey(cmd.User.Id)) + { + var randomServer = _botServices.LodestoneServers[_botServices.Random.Next(_botServices.LodestoneServers.Length)]; + var response = await req.GetAsync($"https://{randomServer}.finalfantasyxiv.com/lodestone/character/{_botServices.DiscordLodestoneMapping[cmd.User.Id]}").ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (content.Contains(lodestoneAuth.LodestoneAuthString)) + { + _botServices.DiscordLodestoneMapping.TryRemove(cmd.User.Id, out _); + + var user = new User(); + + var hasValidUid = false; + while (!hasValidUid) + { + var uid = StringUtils.GenerateRandomString(10); + if (db.Users.Any(u => u.UID == uid || u.Alias == 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 (_botServices.Configuration.GetValue("PurgeUnusedAccounts")) + { + var purgedDays = _botServices.Configuration.GetValue("PurgeUnusedAccountsPeriodInDays"); + user.LastLoggedIn = DateTime.UtcNow - TimeSpan.FromDays(purgedDays) + TimeSpan.FromDays(1); + } + + var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString()); + var auth = new Auth() + { + HashedKey = StringUtils.Sha256String(computedHash), + User = user, + }; + + await db.Users.AddAsync(user).ConfigureAwait(false); + await db.Auth.AddAsync(auth).ConfigureAwait(false); + + _botServices.Logger.LogInformation("User registered: {userUID}", user.UID); + + _botServices.Metrics.IncGauge(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**"); + _botServices.DiscordLodestoneMapping.TryRemove(cmd.User.Id, out _); + } + + var dataEmbed = embedBuilder.Build(); + + await cmd.FollowupAsync(embed: dataEmbed, ephemeral: true).ConfigureAwait(false); + } } From 76dded084073fff4c35b4615f0c72f71aa228fce Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Sat, 19 Nov 2022 13:11:08 +0100 Subject: [PATCH 3/5] fix verify relink --- .../MareSynchronosServices/Discord/MareModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs index fa254e0..6dd326d 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs @@ -111,7 +111,7 @@ public class MareModule : InteractionModuleBase eb.WithDescription("You are already queued for verification. Please wait."); await RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); } - else if (!_botServices.DiscordLodestoneMapping.ContainsKey(Context.User.Id)) + else if (!_botServices.DiscordRelinkLodestoneMapping.ContainsKey(Context.User.Id)) { eb.WithTitle("Cannot verify relink"); eb.WithDescription("You need to **/relink** first before you can **/verify_relink**"); From a554d751b4a22774cd6f8b612296cc5a615d659a Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Sat, 19 Nov 2022 13:24:20 +0100 Subject: [PATCH 4/5] fix service provider dispoal --- .../Discord/DiscordBotServices.cs | 9 ++++++--- .../MareSynchronosServices/Discord/MareModule.cs | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs index 59ec3d3..5f5606f 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs @@ -11,21 +11,24 @@ namespace MareSynchronosServices.Discord; public class DiscordBotServices { - public readonly ConcurrentQueue> verificationQueue = new(); + public readonly ConcurrentQueue>> verificationQueue = new(); public ConcurrentDictionary LastVanityChange = new(); public ConcurrentDictionary LastVanityGidChange = new(); public ConcurrentDictionary DiscordLodestoneMapping = new(); public ConcurrentDictionary DiscordRelinkLodestoneMapping = new(); public readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; + private readonly IServiceProvider _serviceProvider; + public IConfiguration Configuration { get; init; } public ILogger Logger { get; init; } public MareMetrics Metrics { get; init; } public Random Random { get; init; } private CancellationTokenSource? verificationTaskCts; - public DiscordBotServices(IConfiguration configuration, ILogger logger, MareMetrics metrics) + public DiscordBotServices(IConfiguration configuration, IServiceProvider serviceProvider, ILogger logger, MareMetrics metrics) { Configuration = configuration.GetRequiredSection("MareSynchronos"); + _serviceProvider = serviceProvider; Logger = logger; Metrics = metrics; Random = new(); @@ -50,7 +53,7 @@ public class DiscordBotServices { try { - queueitem.Value.Invoke(); + queueitem.Value.Invoke(_serviceProvider); Logger.LogInformation("Sent login information to user"); } diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs b/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs index 6dd326d..8ba4b8c 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/MareModule.cs @@ -97,7 +97,7 @@ public class MareModule : InteractionModuleBase else { await DeferAsync(ephemeral: true).ConfigureAwait(false); - _botServices.verificationQueue.Enqueue(new KeyValuePair(Context.User.Id, async () => await HandleVerifyAsync((SocketSlashCommand)Context.Interaction))); + _botServices.verificationQueue.Enqueue(new KeyValuePair>(Context.User.Id, async (sp) => await HandleVerifyAsync((SocketSlashCommand)Context.Interaction, sp))); } } @@ -120,7 +120,7 @@ public class MareModule : InteractionModuleBase else { await DeferAsync(ephemeral: true).ConfigureAwait(false); - _botServices.verificationQueue.Enqueue(new KeyValuePair(Context.User.Id, async () => await HandleVerifyRelinkAsync((SocketSlashCommand)Context.Interaction))); + _botServices.verificationQueue.Enqueue(new KeyValuePair>(Context.User.Id, async (sp) => await HandleVerifyRelinkAsync((SocketSlashCommand)Context.Interaction, sp))); } } @@ -608,11 +608,11 @@ public class MareModule : InteractionModuleBase } } - private async Task HandleVerifyRelinkAsync(SocketSlashCommand cmd) + private async Task HandleVerifyRelinkAsync(SocketSlashCommand cmd, IServiceProvider serviceProvider) { var embedBuilder = new EmbedBuilder(); - using var scope = _services.CreateScope(); + using var scope = serviceProvider.CreateScope(); var req = new HttpClient(); using var db = scope.ServiceProvider.GetService(); @@ -674,7 +674,7 @@ public class MareModule : InteractionModuleBase { embedBuilder.WithTitle("Your auth has expired or something else went wrong"); embedBuilder.WithDescription("Start again with **/relink**"); - _botServices.DiscordLodestoneMapping.TryRemove(cmd.User.Id, out _); + _botServices.DiscordRelinkLodestoneMapping.TryRemove(cmd.User.Id, out _); } var dataEmbed = embedBuilder.Build(); @@ -682,11 +682,11 @@ public class MareModule : InteractionModuleBase await cmd.FollowupAsync(embed: dataEmbed, ephemeral: true).ConfigureAwait(false); } - private async Task HandleVerifyAsync(SocketSlashCommand cmd) + private async Task HandleVerifyAsync(SocketSlashCommand cmd, IServiceProvider serviceProvider) { var embedBuilder = new EmbedBuilder(); - using var scope = _services.CreateScope(); + using var scope = serviceProvider.CreateScope(); var req = new HttpClient(); using var db = scope.ServiceProvider.GetService(); From 5bd9c7d09dd2331a05e17587543550b860ef384a Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Sun, 11 Dec 2022 13:17:52 +0100 Subject: [PATCH 5/5] add positive response caching to grpc auth service to lessen load --- .../Services/GrpcAuthenticationService.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/MareSynchronosServer/MareSynchronosShared/Services/GrpcAuthenticationService.cs b/MareSynchronosServer/MareSynchronosShared/Services/GrpcAuthenticationService.cs index 8631346..40cf5b1 100644 --- a/MareSynchronosServer/MareSynchronosShared/Services/GrpcAuthenticationService.cs +++ b/MareSynchronosServer/MareSynchronosShared/Services/GrpcAuthenticationService.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Security.Cryptography; using MareSynchronosShared.Protos; using Microsoft.Extensions.Logging; @@ -13,9 +12,16 @@ public class GrpcAuthenticationService : GrpcBaseService public long Id { get; set; } } + private record AuthResponseCache + { + public AuthReply Response { get; set; } + public DateTime WrittenTo { get; set; } + } + private readonly AuthService.AuthServiceClient _authClient; private readonly ConcurrentQueue _requestQueue = new(); private readonly ConcurrentDictionary _authReplies = new(); + private readonly ConcurrentDictionary _cachedPositiveResponses = new(StringComparer.Ordinal); private long _requestId = 0; public GrpcAuthenticationService(ILogger logger, AuthService.AuthServiceClient authClient) : base(logger) @@ -25,7 +31,12 @@ public class GrpcAuthenticationService : GrpcBaseService public async Task AuthorizeAsync(string ip, string secretKey) { - using var sha1 = SHA1.Create(); + if (_cachedPositiveResponses.TryGetValue(secretKey, out var cachedPositiveResponse)) + { + if (cachedPositiveResponse.WrittenTo.AddMinutes(5) < DateTime.UtcNow) return cachedPositiveResponse.Response; + _cachedPositiveResponses.Remove(secretKey, out _); + } + var id = Interlocked.Increment(ref _requestId); _requestQueue.Enqueue(new AuthRequestInternal() { @@ -45,6 +56,15 @@ public class GrpcAuthenticationService : GrpcBaseService await Task.Delay(10, cts.Token).ConfigureAwait(false); } + if (response?.Success ?? false) + { + _cachedPositiveResponses[secretKey] = new AuthResponseCache + { + Response = response, + WrittenTo = DateTime.UtcNow + }; + } + return response ?? new AuthReply { Success = false,