diff --git a/MareSynchronosServer/MareSynchronosServer.sln b/MareSynchronosServer/MareSynchronosServer.sln index 8f013f3..4e7c962 100644 --- a/MareSynchronosServer/MareSynchronosServer.sln +++ b/MareSynchronosServer/MareSynchronosServer.sln @@ -7,7 +7,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServer", "Mar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{326BFB1B-5571-47A6-8513-1FFDB32D53B0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosServerTest", "MareSynchronosServerTest\MareSynchronosServerTest.csproj", "{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServerTest", "MareSynchronosServerTest\MareSynchronosServerTest.csproj", "{25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}" +EndProject +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 @@ -27,6 +33,18 @@ Global {25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Debug|Any CPU.Build.0 = Debug|Any CPU {25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Release|Any CPU.ActiveCfg = Release|Any CPU {25A82A2A-35C2-4EE0-A0E8-DFDD77978DDA}.Release|Any CPU.Build.0 = Release|Any CPU + {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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/Authentication/SecretKeyAuthenticationHandler.cs b/MareSynchronosServer/MareSynchronosServer/Authentication/SecretKeyAuthenticationHandler.cs deleted file mode 100644 index 55371fb..0000000 --- a/MareSynchronosServer/MareSynchronosServer/Authentication/SecretKeyAuthenticationHandler.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading; -using System.Threading.Tasks; -using MareSynchronosServer.Data; -using MareSynchronosServer.Metrics; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace MareSynchronosServer.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); - if (token.IsCancellationRequested) return; - FailedAuthorization fauth; - lock (failedAuthLock) - { - FailedAuthorizations.Remove(ip, out fauth); - } - fauth.Dispose(); - }, token); - - Logger.LogWarning("TempBan " + ip + " for authorization spam"); - 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); - 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))?.UserUID; - - if (uid == null) - { - lock (authDictLock) - { - Authentications[hashedHeader] = unauthorized; - } - - Logger.LogWarning("Failed authorization from " + 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/MareSynchronosServer/Data/MareDbContext.cs b/MareSynchronosServer/MareSynchronosServer/Data/MareDbContext.cs deleted file mode 100644 index 571436a..0000000 --- a/MareSynchronosServer/MareSynchronosServer/Data/MareDbContext.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MareSynchronosServer.Models; -using Microsoft.EntityFrameworkCore; - -namespace MareSynchronosServer.Data -{ - public class MareDbContext : DbContext - { - public MareDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet Users { get; set; } - public DbSet Files { get; set; } - public DbSet ClientPairs { get; set; } - public DbSet ForbiddenUploadEntries { get; set; } - public DbSet BannedUsers { get; set; } - public DbSet Auth { get; set; } - public DbSet LodeStoneAuth { get; set; } - public DbSet BannedRegistrations { get; set; } - - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().ToTable("auth"); - modelBuilder.Entity().ToTable("users"); - modelBuilder.Entity().HasIndex(c => c.CharacterIdentification); - modelBuilder.Entity().ToTable("file_caches"); - modelBuilder.Entity().HasIndex(c => c.UploaderUID); - modelBuilder.Entity().ToTable("client_pairs"); - modelBuilder.Entity().HasKey(u => new { u.UserUID, u.OtherUserUID }); - modelBuilder.Entity().HasIndex(c => c.UserUID); - modelBuilder.Entity().HasIndex(c => c.OtherUserUID); - modelBuilder.Entity().ToTable("forbidden_upload_entries"); - modelBuilder.Entity().ToTable("banned_users"); - modelBuilder.Entity().ToTable("lodestone_auth"); - modelBuilder.Entity().ToTable("banned_registrations"); - } - } -} diff --git a/MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs b/MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs deleted file mode 100644 index 67dd217..0000000 --- a/MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs +++ /dev/null @@ -1,431 +0,0 @@ -using Discord; -using Discord.WebSocket; -using MareSynchronosServer.Data; -using MareSynchronosServer.Hubs; -using MareSynchronosServer.Metrics; -using MareSynchronosServer.Models; -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; - -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(); - try - { - if (arg.Data.Name == "register") - { - if (arg.Data.Options.FirstOrDefault(f => f.Name == "overwrite_old_account") != null) - { - await DeletePreviousUserAccount(arg.User.Id); - } - - 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()); - } - 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); - } - 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); - } - else - { - await arg.DeferAsync(ephemeral: true); - verificationQueue.Enqueue(arg); - } - } - else - { - await arg.RespondAsync("idk what you did to get here to start, just follow the instructions as provided.", ephemeral: true); - } - } - 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); - if (discordAuthedUser != null) - { - if (discordAuthedUser.User != null) - { - CleanupService.PurgeUser(discordAuthedUser.User, db, configuration); - } - else - { - db.Remove(discordAuthedUser); - } - - await db.SaveChangesAsync(); - } - } - - private async Task DiscordClient_ModalSubmitted(SocketModal arg) - { - if (arg.Data.CustomId == "register_modal") - { - var embed = await HandleRegisterModalAsync(arg); - await arg.RespondAsync(embeds: new Embed[] { embed }, ephemeral: true); - } - } - - 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]}"); - if (response.IsSuccessStatusCode) - { - var content = await response.Content.ReadAsStringAsync(); - 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()) - { - 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, - }; - - db.Users.Add(user); - db.Auth.Add(auth); - - logger.LogInformation("User registered: " + 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(); - } - 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); - // 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(); - - 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()); - await discordClient.CreateGlobalApplicationCommandAsync(verify.Build()); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to create command"); - } - } - - private Task Log(LogMessage msg) - { - logger.LogInformation(msg.ToString()); - - return Task.CompletedTask; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - if (!string.IsNullOrEmpty(authToken)) - { - authToken = configuration.GetValue("DiscordBotToken"); - - await discordClient.LoginAsync(TokenType.Bot, authToken); - await discordClient.StartAsync(); - - 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); - await queueitem.FollowupAsync(embed: dataEmbed, ephemeral: true); - - logger.LogInformation("Sent login information to user"); - } - catch (Exception e) - { - logger.LogError(e.Message); - } - - } - await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token); - } - } - - private async Task UpdateStatusAsync() - { - updateStatusCts = new(); - while (!updateStatusCts.IsCancellationRequested) - { - using var scope = services.CreateScope(); - using var db = scope.ServiceProvider.GetService(); - - var users = db.Users.Count(c => c.CharacterIdentification != null); - - await discordClient.SetActivityAsync(new Game("Mare for " + users + " Users")); - - await Task.Delay(TimeSpan.FromSeconds(15)); - } - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - verificationTaskCts?.Cancel(); - updateStatusCts?.Cancel(); - - await discordClient.LogoutAsync(); - await discordClient.StopAsync(); - } - } -} diff --git a/MareSynchronosServer/MareSynchronosServer/Authentication/IdBasedUserIdProvider.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/IdBasedUserIdProvider.cs similarity index 88% rename from MareSynchronosServer/MareSynchronosServer/Authentication/IdBasedUserIdProvider.cs rename to MareSynchronosServer/MareSynchronosServer/Hubs/IdBasedUserIdProvider.cs index 05aef38..ef08d56 100644 --- a/MareSynchronosServer/MareSynchronosServer/Authentication/IdBasedUserIdProvider.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/IdBasedUserIdProvider.cs @@ -2,7 +2,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.SignalR; -namespace MareSynchronosServer.Authentication +namespace MareSynchronosServer.Hubs { public class IdBasedUserIdProvider : IUserIdProvider { diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs index 6877900..4600284 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs @@ -2,8 +2,8 @@ using System.Linq; using System.Threading.Tasks; using MareSynchronos.API; -using MareSynchronosServer.Authentication; -using MareSynchronosServer.Models; +using MareSynchronosShared.Authentication; +using MareSynchronosShared.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; @@ -18,58 +18,58 @@ 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) { if (!IsAdmin) return; - var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid); + var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false); if (user == null) return; user.IsModerator = isModerator; _dbContext.Update(user); - await _dbContext.SaveChangesAsync(); - await Clients.Users(user.UID).SendAsync(Api.OnAdminForcedReconnect); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + 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) { if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return; var existingUser = - await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash); + await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false); if (existingUser == null) { return; } _dbContext.Remove(existingUser); - await _dbContext.SaveChangesAsync(); - await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteBannedUser, dto); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + 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) { if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return; var existingFile = - await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash); + await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false); if (existingFile == null) { return; } _dbContext.Remove(existingFile); - await _dbContext.SaveChangesAsync(); - await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteForbiddenFile, dto); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + 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() { @@ -79,10 +79,10 @@ namespace MareSynchronosServer.Hubs { CharacterHash = b.CharacterIdentification, Reason = b.Reason - }).ToListAsync(); + }).ToListAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeAdminGetForbiddenFiles)] public async Task> GetForbiddenFiles() { @@ -92,10 +92,10 @@ namespace MareSynchronosServer.Hubs { Hash = b.Hash, ForbiddenBy = b.ForbiddenBy - }).ToListAsync(); + }).ToListAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeAdminGetOnlineUsers)] public async Task> AdminGetOnlineUsers() { @@ -107,17 +107,17 @@ namespace MareSynchronosServer.Hubs UID = b.UID, IsModerator = b.IsModerator, IsAdmin = b.IsAdmin - }).ToListAsync(); + }).ToListAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendAdminUpdateOrAddBannedUser)] public async Task UpdateOrAddBannedUser(BannedUserDto dto) { if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return; var existingUser = - await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash); + await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false); if (existingUser != null) { existingUser.Reason = dto.Reason; @@ -129,27 +129,27 @@ namespace MareSynchronosServer.Hubs { CharacterIdentification = dto.CharacterHash, Reason = dto.Reason - }); + }).ConfigureAwait(false); } - await _dbContext.SaveChangesAsync(); - await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddBannedUser, dto); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddBannedUser, dto).ConfigureAwait(false); var bannedUser = - await _dbContext.Users.SingleOrDefaultAsync(u => u.CharacterIdentification == dto.CharacterHash); + await _dbContext.Users.SingleOrDefaultAsync(u => u.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false); if (bannedUser != null) { - await Clients.User(bannedUser.UID).SendAsync(Api.OnAdminForcedReconnect); + await Clients.User(bannedUser.UID).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false); } } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendAdminUpdateOrAddForbiddenFile)] public async Task UpdateOrAddForbiddenFile(ForbiddenFileDto dto) { if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return; var existingForbiddenFile = - await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash); + await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false); if (existingForbiddenFile != null) { existingForbiddenFile.ForbiddenBy = dto.ForbiddenBy; @@ -161,12 +161,12 @@ namespace MareSynchronosServer.Hubs { Hash = dto.Hash, ForbiddenBy = dto.ForbiddenBy - }); + }).ConfigureAwait(false); } - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); - await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddForbiddenFile, dto); + await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddForbiddenFile, dto).ConfigureAwait(false); } } } diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs index bc5027b..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.Authentication; -using MareSynchronosServer.Metrics; -using MareSynchronosServer.Models; +using MareSynchronosShared.Authentication; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Models; +using MareSynchronosShared.Protos; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; @@ -21,45 +22,47 @@ namespace MareSynchronosServer.Hubs { private string BasePath => _configuration["CacheDirectory"]; - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendFileAbortUpload)] public async Task AbortUpload() { - _logger.LogInformation("User " + AuthenticatedUserId + " aborted upload"); + _logger.LogInformation("User {AuthenticatedUserId} aborted upload", AuthenticatedUserId); var userId = AuthenticatedUserId; var notUploadedFiles = _dbContext.Files.Where(f => !f.Uploaded && f.Uploader.UID == userId).ToList(); _dbContext.RemoveRange(notUploadedFiles); - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendFileDeleteAllFiles)] public async Task DeleteAllFiles() { - _logger.LogInformation("User " + AuthenticatedUserId + " deleted all their files"); + _logger.LogInformation("User {AuthenticatedUserId} deleted all their files", AuthenticatedUserId); - var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync(); + var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false); foreach (var file in ownFiles) { 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(); } } _dbContext.Files.RemoveRange(ownFiles); - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeGetFilesSizes)] public async Task> GetFilesSizes(List hashes) { - var allFiles = await _dbContext.Files.Where(f => hashes.Contains(f.Hash)).ToListAsync(); + var allFiles = await _dbContext.Files.Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false); var forbiddenFiles = await _dbContext.ForbiddenUploadEntries. - Where(f => hashes.Contains(f.Hash)).ToListAsync(); + Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false); List response = new(); foreach (var hash in hashes) { @@ -90,33 +93,32 @@ namespace MareSynchronosServer.Hubs if (!fileInfo.Exists && downloadFile != null) { _dbContext.Files.Remove(downloadFile); - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); } } return response; } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeFileIsUploadFinished)] public async Task IsUploadFinished() { var userUid = AuthenticatedUserId; return await _dbContext.Files.AsNoTracking() - .AnyAsync(f => f.Uploader.UID == userUid && !f.Uploaded); + .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) { var userSentHashes = new HashSet(fileListHashes.Distinct()); - _logger.LogInformation($"User {AuthenticatedUserId} sending files: {userSentHashes.Count}"); + _logger.LogInformation("User {AuthenticatedUserId} sending files: {count}", AuthenticatedUserId, userSentHashes.Count); var notCoveredFiles = new Dictionary(); - // Todo: Check if a select can directly transform to hashset - var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f); - var existingFiles = await _dbContext.Files.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f); - var uploader = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId); + var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false); + var existingFiles = await _dbContext.Files.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false); + var uploader = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); List fileCachesToUpload = new(); foreach (var file in userSentHashes) @@ -137,7 +139,7 @@ namespace MareSynchronosServer.Hubs } if (existingFiles.ContainsKey(file)) { continue; } - _logger.LogInformation("User " + AuthenticatedUserId + " needs upload: " + file); + _logger.LogInformation("User {AuthenticatedUserId} needs upload: {file}", AuthenticatedUserId, file); var userId = AuthenticatedUserId; fileCachesToUpload.Add(new FileCache() { @@ -152,16 +154,16 @@ namespace MareSynchronosServer.Hubs }; } //Save bulk - await _dbContext.Files.AddRangeAsync(fileCachesToUpload); - await _dbContext.SaveChangesAsync(); + await _dbContext.Files.AddRangeAsync(fileCachesToUpload).ConfigureAwait(false); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); return notCoveredFiles.Values.ToList(); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendFileUploadFileStreamAsync)] public async Task UploadFileStreamAsync(string hash, IAsyncEnumerable fileContent) { - _logger.LogInformation("User " + AuthenticatedUserId + " uploading file: " + hash); + _logger.LogInformation("User {AuthenticatedUserId} uploading file: {hash}", AuthenticatedUserId, hash); var relatedFile = _dbContext.Files.SingleOrDefault(f => f.Hash == hash && f.Uploader.UID == AuthenticatedUserId && f.Uploaded == false); if (relatedFile == null) return; @@ -173,23 +175,23 @@ namespace MareSynchronosServer.Hubs long length = 0; try { - await foreach (var chunk in fileContent) + await foreach (var chunk in fileContent.ConfigureAwait(false)) { length += chunk.Length; - await fileStream.WriteAsync(chunk); + await fileStream.WriteAsync(chunk).ConfigureAwait(false); } - await fileStream.FlushAsync(); - await fileStream.DisposeAsync(); + await fileStream.FlushAsync().ConfigureAwait(false); + await fileStream.DisposeAsync().ConfigureAwait(false); } catch { try { - await fileStream.FlushAsync(); - await fileStream.DisposeAsync(); + await fileStream.FlushAsync().ConfigureAwait(false); + await fileStream.DisposeAsync().ConfigureAwait(false); _dbContext.Files.Remove(relatedFile); - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); } catch { @@ -203,20 +205,20 @@ namespace MareSynchronosServer.Hubs return; } - _logger.LogInformation("User " + AuthenticatedUserId + " upload finished: " + hash + ", size: " + length); + _logger.LogInformation("User {AuthenticatedUserId} upload finished: {hash}, size: {length}", AuthenticatedUserId, hash, length); try { - var decodedFile = LZ4.LZ4Codec.Unwrap(await File.ReadAllBytesAsync(tempFileName)); + var decodedFile = LZ4.LZ4Codec.Unwrap(await File.ReadAllBytesAsync(tempFileName).ConfigureAwait(false)); using var sha1 = SHA1.Create(); using var ms = new MemoryStream(decodedFile); - var computedHash = await sha1.ComputeHashAsync(ms); + var computedHash = await sha1.ComputeHashAsync(ms).ConfigureAwait(false); var computedHashString = BitConverter.ToString(computedHash).Replace("-", ""); if (hash != computedHashString) { - _logger.LogWarning($"Computed file hash was not expected file hash. Computed: {computedHashString}, Expected {hash}"); + _logger.LogWarning("Computed file hash was not expected file hash. Computed: {computedHashString}, Expected {hash}", computedHashString, hash); _dbContext.Remove(relatedFile); - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); return; } @@ -225,17 +227,19 @@ 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(); - _logger.LogInformation("File " + hash + " added to DB"); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + _logger.LogInformation("File {hash} added to DB", hash); } catch (Exception ex) { _logger.LogWarning(ex, "Upload failed"); _dbContext.Remove(relatedFile); - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); } } } diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs index 2b267b1..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.Authentication; -using MareSynchronosServer.Metrics; -using MareSynchronosServer.Models; +using MareSynchronosShared.Authentication; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Models; +using MareSynchronosShared.Protos; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; @@ -14,18 +15,18 @@ namespace MareSynchronosServer.Hubs { public partial class MareHub { - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.SendUserDeleteAccount)] public async Task DeleteAccount() { - _logger.LogInformation("User " + AuthenticatedUserId + " deleted their account"); + _logger.LogInformation("User {AuthenticatedUserId} deleted their account", AuthenticatedUserId); string userid = AuthenticatedUserId; - var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid); - var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == userid).ToListAsync(); - var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == userid); - var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == userid); + var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid).ConfigureAwait(false); + var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == userid).ToListAsync().ConfigureAwait(false); + var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == userid).ConfigureAwait(false); + var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == userid).ConfigureAwait(false); if (lodestone != null) { @@ -34,18 +35,16 @@ namespace MareSynchronosServer.Hubs while (_dbContext.Files.Any(f => f.Uploader == userEntry)) { - await Task.Delay(1000); + 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(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); var otherPairData = await _dbContext.ClientPairs.Include(u => u.User) - .Where(u => u.OtherUser.UID == userid).ToListAsync(); + .Where(u => u.OtherUser.UID == userid).ToListAsync().ConfigureAwait(false); foreach (var pair in otherPairData) { await Clients.User(pair.User.UID) @@ -53,42 +52,45 @@ namespace MareSynchronosServer.Hubs { OtherUID = userid, IsRemoved = true - }, userEntry.CharacterIdentification); + }, 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); _dbContext.Remove(auth); - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeUserGetOnlineCharacters)] public async Task> GetOnlineCharacters() { - _logger.LogInformation("User " + AuthenticatedUserId + " requested online characters"); + _logger.LogInformation("User {AuthenticatedUserId} requested online characters", AuthenticatedUserId); - var ownUser = await GetAuthenticatedUserUntrackedAsync(); + var ownUser = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false); var otherUsers = await _dbContext.ClientPairs.AsNoTracking() - .Include(u => u.User) - .Include(u => u.OtherUser) - .Where(w => w.User.UID == ownUser.UID && !w.IsPaused) - .Where(w => !string.IsNullOrEmpty(w.OtherUser.CharacterIdentification)) - .Select(e => e.OtherUser).ToListAsync(); + .Include(u => u.User) + .Include(u => u.OtherUser) + .Where(w => w.User.UID == ownUser.UID && !w.IsPaused) + .Where(w => !string.IsNullOrEmpty(w.OtherUser.CharacterIdentification)) + .Select(e => e.OtherUser).ToListAsync().ConfigureAwait(false); var otherEntries = await _dbContext.ClientPairs.AsNoTracking() .Include(u => u.User) - .Where(u => otherUsers.Any(e => e == u.User) && u.OtherUser == ownUser && !u.IsPaused).ToListAsync(); + .Where(u => otherUsers.Any(e => e == u.User) && u.OtherUser == ownUser && !u.IsPaused).ToListAsync().ConfigureAwait(false); - await Clients.Users(otherEntries.Select(e => e.User.UID)).SendAsync(Api.OnUserAddOnlinePairedPlayer, ownUser.CharacterIdentification); + await Clients.Users(otherEntries.Select(e => e.User.UID)).SendAsync(Api.OnUserAddOnlinePairedPlayer, ownUser.CharacterIdentification).ConfigureAwait(false); return otherEntries.Select(e => e.User.CharacterIdentification).Distinct().ToList(); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeUserGetPairedClients)] public async Task> GetPairedClients() { @@ -117,7 +119,7 @@ namespace MareSynchronosServer.Hubs IsSynced = otherEntry != null }; - return (await query.ToListAsync()).Select(f => new ClientPairDto() + return (await query.ToListAsync().ConfigureAwait(false)).Select(f => new ClientPairDto() { IsPaused = f.IsPaused, OtherUID = f.OtherUserUID, @@ -126,13 +128,13 @@ namespace MareSynchronosServer.Hubs }).ToList(); } - [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [HubMethodName(Api.InvokeUserPushCharacterDataToVisibleClients)] public async Task PushCharacterDataToVisibleClients(CharacterCacheDto characterCache, List visibleCharacterIds) { - _logger.LogInformation("User " + AuthenticatedUserId + " pushing character data to " + visibleCharacterIds.Count + " visible clients"); + _logger.LogInformation("User {AuthenticatedUserId} pushing character data to {visibleCharacterIds} visible clients", AuthenticatedUserId, visibleCharacterIds.Count); - var user = await GetAuthenticatedUserUntrackedAsync(); + var user = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false); var query = from userToOther in _dbContext.ClientPairs @@ -154,38 +156,40 @@ namespace MareSynchronosServer.Hubs && visibleCharacterIds.Contains(userToOther.OtherUser.CharacterIdentification) select otherToUser.UserUID; - var otherEntries = await query.ToListAsync(); + var otherEntries = await query.ToListAsync().ConfigureAwait(false); - await Clients.Users(otherEntries).SendAsync(Api.OnUserReceiveCharacterData, characterCache, user.CharacterIdentification); + 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) { if (uid == AuthenticatedUserId) return; uid = uid.Trim(); - var user = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId); + var user = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); var otherUser = await _dbContext.Users - .SingleOrDefaultAsync(u => u.UID == uid); + .SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false); var existingEntry = await _dbContext.ClientPairs.AsNoTracking() .FirstOrDefaultAsync(p => - p.User.UID == AuthenticatedUserId && p.OtherUser.UID == uid); + p.User.UID == AuthenticatedUserId && p.OtherUser.UID == uid).ConfigureAwait(false); if (otherUser == null || existingEntry != null) return; - _logger.LogInformation("User " + AuthenticatedUserId + " adding " + uid + " to whitelist"); + _logger.LogInformation("User {AuthenticatedUserId} adding {uid} to whitelist", AuthenticatedUserId, uid); ClientPair wl = new ClientPair() { IsPaused = false, OtherUser = otherUser, User = user }; - await _dbContext.ClientPairs.AddAsync(wl); - await _dbContext.SaveChangesAsync(); + await _dbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); var otherEntry = OppositeEntry(uid); await Clients.User(user.UID) .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() @@ -194,7 +198,7 @@ namespace MareSynchronosServer.Hubs IsPaused = false, IsPausedFromOthers = otherEntry?.IsPaused ?? false, IsSynced = otherEntry != null - }, string.Empty); + }, string.Empty).ConfigureAwait(false); if (otherEntry != null) { await Clients.User(uid).SendAsync(Api.OnUserUpdateClientPairs, @@ -204,34 +208,34 @@ namespace MareSynchronosServer.Hubs IsPaused = otherEntry.IsPaused, IsPausedFromOthers = false, IsSynced = true - }, user.CharacterIdentification); + }, user.CharacterIdentification).ConfigureAwait(false); if (!string.IsNullOrEmpty(otherUser.CharacterIdentification)) { await Clients.User(user.UID) - .SendAsync(Api.OnUserAddOnlinePairedPlayer, otherUser.CharacterIdentification); + .SendAsync(Api.OnUserAddOnlinePairedPlayer, otherUser.CharacterIdentification).ConfigureAwait(false); await Clients.User(otherUser.UID) - .SendAsync(Api.OnUserAddOnlinePairedPlayer, user.CharacterIdentification); + .SendAsync(Api.OnUserAddOnlinePairedPlayer, user.CharacterIdentification).ConfigureAwait(false); } } - 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) { if (otherUserUid == AuthenticatedUserId) return; - ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid); + ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false); if (pair == null) return; - _logger.LogInformation("User " + AuthenticatedUserId + " changed pause status with " + otherUserUid + " to " + isPaused); + _logger.LogInformation("User {AuthenticatedUserId} changed pause status with {otherUserUid} to {isPaused}", AuthenticatedUserId, otherUserUid, isPaused); pair.IsPaused = isPaused; _dbContext.Update(pair); - await _dbContext.SaveChangesAsync(); - var selfCharaIdent = (await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId)).CharacterIdentification; - var otherCharaIdent = (await _dbContext.Users.SingleAsync(u => u.UID == otherUserUid)).CharacterIdentification; + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + var selfCharaIdent = (await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false)).CharacterIdentification; + var otherCharaIdent = (await _dbContext.Users.SingleAsync(u => u.UID == otherUserUid).ConfigureAwait(false)).CharacterIdentification; var otherEntry = OppositeEntry(otherUserUid); await Clients.User(AuthenticatedUserId) @@ -241,7 +245,7 @@ namespace MareSynchronosServer.Hubs IsPaused = isPaused, IsPausedFromOthers = otherEntry?.IsPaused ?? false, IsSynced = otherEntry != null - }, otherCharaIdent); + }, otherCharaIdent).ConfigureAwait(false); if (otherEntry != null) { await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() @@ -250,60 +254,60 @@ namespace MareSynchronosServer.Hubs IsPaused = otherEntry.IsPaused, IsPausedFromOthers = isPaused, IsSynced = true - }, selfCharaIdent); + }, selfCharaIdent).ConfigureAwait(false); } 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) { if (uid == AuthenticatedUserId) return; - var sender = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId); - var otherUser = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid); + var sender = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); + var otherUser = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false); if (otherUser == null) return; - _logger.LogInformation("User " + AuthenticatedUserId + " removed " + uid + " from whitelist"); + _logger.LogInformation("User {AuthenticatedUserId} removed {uid} from whitelist", AuthenticatedUserId, uid); ClientPair wl = - await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.User == sender && w.OtherUser == otherUser); + await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.User == sender && w.OtherUser == otherUser).ConfigureAwait(false); if (wl == null) return; _dbContext.ClientPairs.Remove(wl); - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); var otherEntry = OppositeEntry(uid); await Clients.User(sender.UID) .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() { OtherUID = otherUser.UID, IsRemoved = true - }, otherUser.CharacterIdentification); + }, otherUser.CharacterIdentification).ConfigureAwait(false); if (otherEntry != null) { if (!string.IsNullOrEmpty(otherUser.CharacterIdentification)) { await Clients.User(sender.UID) - .SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherUser.CharacterIdentification); + .SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherUser.CharacterIdentification).ConfigureAwait(false); await Clients.User(otherUser.UID) - .SendAsync(Api.OnUserRemoveOnlinePairedPlayer, sender.CharacterIdentification); + .SendAsync(Api.OnUserRemoveOnlinePairedPlayer, sender.CharacterIdentification).ConfigureAwait(false); await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() { OtherUID = sender.UID, IsPaused = otherEntry.IsPaused, IsPausedFromOthers = false, IsSynced = false - }, sender.CharacterIdentification); + }, sender.CharacterIdentification).ConfigureAwait(false); } } - 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 abce674..413702c 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs @@ -4,9 +4,11 @@ using System.Security.Claims; using System.Security.Cryptography; using System.Threading.Tasks; using MareSynchronos.API; -using MareSynchronosServer.Authentication; -using MareSynchronosServer.Data; -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; @@ -18,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; @@ -34,22 +41,22 @@ 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; - _logger.LogInformation("Connection from " + userId + ", CI: " + characterIdentification); + _logger.LogInformation("Connection from {userId}, CI: {characterIdentification}", userId, characterIdentification); - await Clients.Caller.SendAsync(Api.OnUpdateSystemInfo, _systemInfoService.SystemInfoDto); + await Clients.Caller.SendAsync(Api.OnUpdateSystemInfo, _systemInfoService.SystemInfoDto).ConfigureAwait(false); - var isBanned = await _dbContext.BannedUsers.AsNoTracking().AnyAsync(u => u.CharacterIdentification == characterIdentification); + var isBanned = await _dbContext.BannedUsers.AsNoTracking().AnyAsync(u => u.CharacterIdentification == characterIdentification).ConfigureAwait(false); if (!string.IsNullOrEmpty(userId) && !isBanned && !string.IsNullOrEmpty(characterIdentification)) { - var user = (await _dbContext.Users.SingleAsync(u => u.UID == userId)); + var user = (await _dbContext.Users.SingleAsync(u => u.UID == userId).ConfigureAwait(false)); if (!string.IsNullOrEmpty(user.CharacterIdentification) && characterIdentification != user.CharacterIdentification) { return new ConnectionDto() @@ -59,12 +66,12 @@ 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; user.CharacterIdentification = characterIdentification; - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); return new ConnectionDto { ServerVersion = Api.Version, @@ -80,32 +87,34 @@ namespace MareSynchronosServer.Hubs }; } - public override Task OnConnectedAsync() + public override async Task OnConnectedAsync() { - _logger.LogInformation("Connection from " + contextAccessor.GetIpAddress()); - MareMetrics.Connections.Inc(); - return base.OnConnectedAsync(); + _logger.LogInformation("Connection from {ip}", contextAccessor.GetIpAddress()); + 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); + 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 " + AuthenticatedUserId); + _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 } @@ -114,42 +123,24 @@ namespace MareSynchronosServer.Hubs && !userToOther.IsPaused && !otherToUser.IsPaused select otherToUser.UserUID; - var otherEntries = await query.ToListAsync(); + var otherEntries = await query.ToListAsync().ConfigureAwait(false); + + await Clients.Users(otherEntries).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, user.CharacterIdentification).ConfigureAwait(false); - await Clients.Users(otherEntries).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, user.CharacterIdentification); - _dbContext.RemoveRange(_dbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == user.UID)); user.CharacterIdentification = null; - await _dbContext.SaveChangesAsync(); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); } - await base.OnDisconnectedAsync(exception); - } - - 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); + await base.OnDisconnectedAsync(exception).ConfigureAwait(false); } protected string AuthenticatedUserId => Context.User?.Claims?.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "Unknown"; - protected async Task GetAuthenticatedUserUntrackedAsync() + protected async Task GetAuthenticatedUserUntrackedAsync() { - return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId); + return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); } } } diff --git a/MareSynchronosServer/MareSynchronosServer/Throttling/SignalRLimitFilter.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs similarity index 75% rename from MareSynchronosServer/MareSynchronosServer/Throttling/SignalRLimitFilter.cs rename to MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs index a90cca2..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) @@ -37,25 +37,25 @@ public class SignalRLimitFilter : IHubFilter HttpVerb = "ws", ClientId = invocationContext.Context.UserIdentifier }; - foreach (var rule in await _processor.GetMatchingRulesAsync(client)) + foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false)) { - var counter = await _processor.ProcessRequestAsync(client, rule); + var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false); if (counter.Count > rule.Limit) { var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "Unknown"; var retry = counter.Timestamp.RetryAfterFrom(rule); - logger.LogWarning($"Method rate limit triggered from {ip}/{authUserId}: {invocationContext.HubMethodName}"); + logger.LogWarning("Method rate limit triggered from {ip}/{authUserId}: {method}", ip, authUserId, invocationContext.HubMethodName); throw new HubException($"call limit {retry}"); } } - return await next(invocationContext); + return await next(invocationContext).ConfigureAwait(false); } // Optional method public async Task OnConnectedAsync(HubLifetimeContext context, Func next) { - await ConnectionLimiterSemaphore.WaitAsync(); + await ConnectionLimiterSemaphore.WaitAsync().ConfigureAwait(false); var ip = accessor.GetIpAddress(); var client = new ClientRequestIdentity { @@ -63,13 +63,13 @@ public class SignalRLimitFilter : IHubFilter Path = "Connect", HttpVerb = "ws", }; - foreach (var rule in await _processor.GetMatchingRulesAsync(client)) + foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false)) { - var counter = await _processor.ProcessRequestAsync(client, rule); + var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false); if (counter.Count > rule.Limit) { var retry = counter.Timestamp.RetryAfterFrom(rule); - logger.LogWarning($"Connection rate limit triggered from {ip}"); + logger.LogWarning("Connection rate limit triggered from {ip}", ip); ConnectionLimiterSemaphore.Release(); throw new HubException($"Connection rate limit {retry}"); } @@ -77,8 +77,8 @@ public class SignalRLimitFilter : IHubFilter try { - await Task.Delay(250); - await next(context); + await Task.Delay(250).ConfigureAwait(false); + await next(context).ConfigureAwait(false); } catch (Exception ex) { @@ -93,7 +93,7 @@ public class SignalRLimitFilter : IHubFilter public async Task OnDisconnectedAsync( HubLifetimeContext context, Exception exception, Func next) { - await DisconnectLimiterSemaphore.WaitAsync(); + await DisconnectLimiterSemaphore.WaitAsync().ConfigureAwait(false); if (exception != null) { logger.LogWarning(exception, "InitialException on OnDisconnectedAsync"); @@ -101,8 +101,8 @@ public class SignalRLimitFilter : IHubFilter try { - await next(context, exception); - await Task.Delay(250); + await next(context, exception).ConfigureAwait(false); + await Task.Delay(250).ConfigureAwait(false); } catch (Exception e) { diff --git a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj index fb58143..78e5a49 100644 --- a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj +++ b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj @@ -10,10 +10,15 @@ - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -22,18 +27,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + diff --git a/MareSynchronosServer/MareSynchronosServer/Metrics/MareMetrics.cs b/MareSynchronosServer/MareSynchronosServer/Metrics/MareMetrics.cs deleted file mode 100644 index a78798d..0000000 --- a/MareSynchronosServer/MareSynchronosServer/Metrics/MareMetrics.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.IO; -using System.Linq; -using MareSynchronosServer.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/MareSynchronosServer/Program.cs b/MareSynchronosServer/MareSynchronosServer/Program.cs index 783fcdb..d8c41a8 100644 --- a/MareSynchronosServer/MareSynchronosServer/Program.cs +++ b/MareSynchronosServer/MareSynchronosServer/Program.cs @@ -2,14 +2,12 @@ using System; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using System.Linq; -using MareSynchronosServer.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using MareSynchronosServer.Metrics; -using MareSynchronosServer.Models; using System.Collections.Generic; +using MareSynchronosShared.Data; namespace MareSynchronosServer { @@ -38,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 9b862d1..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; @@ -5,19 +6,16 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using MareSynchronosServer.Authentication; -using MareSynchronosServer.Data; 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 f8a06cf..c8b8e85 100644 --- a/MareSynchronosServer/MareSynchronosServer/SystemInfoService.cs +++ b/MareSynchronosServer/MareSynchronosServer/SystemInfoService.cs @@ -3,9 +3,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MareSynchronos.API; -using MareSynchronosServer.Data; 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; @@ -40,12 +41,16 @@ public class SystemInfoService : IHostedService, IDisposable private void PushSystemInfo(object state) { ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads); - _logger.LogInformation($"ThreadPool: {workerThreads} workers available, {ioThreads} IO workers available"); - MareMetrics.AvailableWorkerThreads.Set(workerThreads); - MareMetrics.AvailableIOWorkerThreads.Set(ioThreads); + _logger.LogInformation("ThreadPool: {workerThreads} workers available, {ioThreads} IO workers available", workerThreads, 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/MareSynchronosServerTest/Hubs/MareHubTest.cs b/MareSynchronosServer/MareSynchronosServerTest/Hubs/MareHubTest.cs index e51407a..4f8394e 100644 --- a/MareSynchronosServer/MareSynchronosServerTest/Hubs/MareHubTest.cs +++ b/MareSynchronosServer/MareSynchronosServerTest/Hubs/MareHubTest.cs @@ -1,6 +1,4 @@ -using MareSynchronosServer.Data; -using MareSynchronosServer.Hubs; -using MareSynchronosServer.Models; +using MareSynchronosServer.Hubs; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; @@ -13,6 +11,8 @@ using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; +using MareSynchronosShared.Data; +using MareSynchronosShared.Models; namespace MareSynchronosServerTest.Hubs { public class MareHubTest { @@ -73,7 +73,7 @@ namespace MareSynchronosServerTest.Hubs { hub.Clients = clientsMock.Object; hub.Context = clientContextMock.Object; - await hub.OnDisconnectedAsync(new Exception("Test Exception")); + await hub.OnDisconnectedAsync(new Exception("Test Exception")).ConfigureAwait(false); clientsMock.Verify(x => x.Users(It.Is>(x => x.Count() == 2 && x[0] == "User2" && x[1] == "User3")), Times.Once); clientProxyMock.Verify(x => x.SendCoreAsync(It.IsAny(), It.Is(o => (string)o[0] == "Ident1"), It.IsAny()), Times.Once); 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 73% rename from MareSynchronosServer/MareSynchronosServer/CleanupService.cs rename to MareSynchronosServer/MareSynchronosServices/CleanupService.cs index a8ccff6..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.Authentication; -using MareSynchronosServer.Data; -using MareSynchronosServer.Metrics; -using MareSynchronosServer.Models; +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; @@ -49,26 +45,28 @@ namespace MareSynchronosServer using var scope = _services.CreateScope(); using var dbContext = scope.ServiceProvider.GetService()!; - _logger.LogInformation($"Cleaning up files older than {filesOlderThanDays} days"); + _logger.LogInformation("Cleaning up files older than {filesOlderThanDays} days", filesOlderThanDays); try { 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) { - _logger.LogInformation("File does not exist anymore: " + fileName); + _logger.LogInformation("File does not exist anymore: {fileName}", fileName); dbContext.Files.Remove(file); } else if (fi.LastAccessTime < prevTime) { - MareMetrics.FilesTotalSize.Dec(fi.Length); - _logger.LogInformation("File outdated: " + fileName); + _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(); } @@ -144,7 +142,7 @@ namespace MareSynchronosServer usersOlderThanDays = 14; } - _logger.LogInformation($"Cleaning up users older than {usersOlderThanDays} days"); + _logger.LogInformation("Cleaning up users older than {usersOlderThanDays} days", usersOlderThanDays); var allUsers = dbContext.Users.ToList(); List usersToRemove = new(); @@ -152,7 +150,7 @@ namespace MareSynchronosServer { if (user.LastLoggedIn < (DateTime.UtcNow - TimeSpan.FromDays(usersOlderThanDays))) { - _logger.LogInformation("User outdated: " + user.UID); + _logger.LogInformation("User outdated: {userUID}", user.UID); usersToRemove.Add(user); } } @@ -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/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/Data/MareDbContext.cs b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs new file mode 100644 index 0000000..b20ca3b --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs @@ -0,0 +1,38 @@ +using MareSynchronosShared.Models; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosShared.Data; + +public class MareDbContext : DbContext +{ + public MareDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Users { get; set; } + public DbSet Files { get; set; } + public DbSet ClientPairs { get; set; } + public DbSet ForbiddenUploadEntries { get; set; } + public DbSet BannedUsers { get; set; } + public DbSet Auth { get; set; } + public DbSet LodeStoneAuth { get; set; } + public DbSet BannedRegistrations { get; set; } + + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("auth"); + modelBuilder.Entity().ToTable("users"); + modelBuilder.Entity().HasIndex(c => c.CharacterIdentification); + modelBuilder.Entity().ToTable("file_caches"); + modelBuilder.Entity().HasIndex(c => c.UploaderUID); + modelBuilder.Entity().ToTable("client_pairs"); + modelBuilder.Entity().HasKey(u => new { u.UserUID, u.OtherUserUID }); + modelBuilder.Entity().HasIndex(c => c.UserUID); + modelBuilder.Entity().HasIndex(c => c.OtherUserUID); + modelBuilder.Entity().ToTable("forbidden_upload_entries"); + modelBuilder.Entity().ToTable("banned_users"); + modelBuilder.Entity().ToTable("lodestone_auth"); + modelBuilder.Entity().ToTable("banned_registrations"); + } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Extensions.cs b/MareSynchronosServer/MareSynchronosShared/Extensions.cs similarity index 100% rename from MareSynchronosServer/MareSynchronosServer/Extensions.cs rename to MareSynchronosServer/MareSynchronosShared/Extensions.cs diff --git a/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj new file mode 100644 index 0000000..afba71f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj @@ -0,0 +1,40 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + Both + + + + 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/MareSynchronosServer/Migrations/20220731210149_InitialCreate.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.Designer.cs similarity index 99% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220731210149_InitialCreate.Designer.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.Designer.cs index ddfded1..31ea23a 100644 --- a/MareSynchronosServer/MareSynchronosServer/Migrations/20220731210149_InitialCreate.Designer.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using MareSynchronosServer.Data; +using MareSynchronosShared.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220731210149_InitialCreate.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs similarity index 100% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220731210149_InitialCreate.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs similarity index 99% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs index 9c8495c..59c7501 100644 --- a/MareSynchronosServer/MareSynchronosServer/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.Designer.cs @@ -1,6 +1,6 @@ // using System; -using MareSynchronosServer.Data; +using MareSynchronosShared.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220731211419_RenameLowerSnakeCase.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.cs similarity index 100% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220731211419_RenameLowerSnakeCase.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220731211419_RenameLowerSnakeCase.cs diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220801121419_AddLodestoneAuth.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs similarity index 99% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220801121419_AddLodestoneAuth.Designer.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs index 0476cdc..5bab84a 100644 --- a/MareSynchronosServer/MareSynchronosServer/Migrations/20220801121419_AddLodestoneAuth.Designer.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.Designer.cs @@ -1,6 +1,6 @@ // using System; -using MareSynchronosServer.Data; +using MareSynchronosShared.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220801121419_AddLodestoneAuth.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs similarity index 100% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220801121419_AddLodestoneAuth.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs similarity index 99% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs index 54c6aa5..3b29f0d 100644 --- a/MareSynchronosServer/MareSynchronosServer/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.Designer.cs @@ -1,6 +1,6 @@ // using System; -using MareSynchronosServer.Data; +using MareSynchronosShared.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs similarity index 100% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs similarity index 99% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.Designer.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs index f7d9af7..82a1dcf 100644 --- a/MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.Designer.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.Designer.cs @@ -1,6 +1,6 @@ // using System; -using MareSynchronosServer.Data; +using MareSynchronosShared.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.cs similarity index 100% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220806103053_AddBannedRegistrations.cs diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs similarity index 99% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs index 6852d4e..66a6821 100644 --- a/MareSynchronosServer/MareSynchronosServer/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.Designer.cs @@ -1,6 +1,6 @@ // using System; -using MareSynchronosServer.Data; +using MareSynchronosShared.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220816170426_SetMaxLimitForStrings.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.cs similarity index 100% rename from MareSynchronosServer/MareSynchronosServer/Migrations/20220816170426_SetMaxLimitForStrings.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/20220816170426_SetMaxLimitForStrings.cs diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/MareDbContextModelSnapshot.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs similarity index 99% rename from MareSynchronosServer/MareSynchronosServer/Migrations/MareDbContextModelSnapshot.cs rename to MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs index 0e6cb67..b01ed50 100644 --- a/MareSynchronosServer/MareSynchronosServer/Migrations/MareDbContextModelSnapshot.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs @@ -1,6 +1,6 @@ // using System; -using MareSynchronosServer.Data; +using MareSynchronosShared.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; diff --git a/MareSynchronosServer/MareSynchronosServer/Models/Auth.cs b/MareSynchronosServer/MareSynchronosShared/Models/Auth.cs similarity index 87% rename from MareSynchronosServer/MareSynchronosServer/Models/Auth.cs rename to MareSynchronosServer/MareSynchronosShared/Models/Auth.cs index f154a73..1f8bf2c 100644 --- a/MareSynchronosServer/MareSynchronosServer/Models/Auth.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/Auth.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosServer.Models +namespace MareSynchronosShared.Models { public class Auth { diff --git a/MareSynchronosServer/MareSynchronosServer/Models/Banned.cs b/MareSynchronosServer/MareSynchronosShared/Models/Banned.cs similarity index 88% rename from MareSynchronosServer/MareSynchronosServer/Models/Banned.cs rename to MareSynchronosServer/MareSynchronosShared/Models/Banned.cs index 63d3958..b56e02a 100644 --- a/MareSynchronosServer/MareSynchronosServer/Models/Banned.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/Banned.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosServer.Models +namespace MareSynchronosShared.Models { public class Banned { diff --git a/MareSynchronosServer/MareSynchronosServer/Models/BannedRegistrations.cs b/MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs similarity index 84% rename from MareSynchronosServer/MareSynchronosServer/Models/BannedRegistrations.cs rename to MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs index f811399..b26283a 100644 --- a/MareSynchronosServer/MareSynchronosServer/Models/BannedRegistrations.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosServer.Models +namespace MareSynchronosShared.Models { public class BannedRegistrations { diff --git a/MareSynchronosServer/MareSynchronosServer/Models/ClientPair.cs b/MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs similarity index 92% rename from MareSynchronosServer/MareSynchronosServer/Models/ClientPair.cs rename to MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs index 0000630..8cabbb5 100644 --- a/MareSynchronosServer/MareSynchronosServer/Models/ClientPair.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosServer.Models +namespace MareSynchronosShared.Models { public class ClientPair { diff --git a/MareSynchronosServer/MareSynchronosServer/Models/FileCache.cs b/MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs similarity index 91% rename from MareSynchronosServer/MareSynchronosServer/Models/FileCache.cs rename to MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs index d84f2a6..a239837 100644 --- a/MareSynchronosServer/MareSynchronosServer/Models/FileCache.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosServer.Models +namespace MareSynchronosShared.Models { public class FileCache { diff --git a/MareSynchronosServer/MareSynchronosServer/Models/ForbiddenUploadEntry.cs b/MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs similarity index 89% rename from MareSynchronosServer/MareSynchronosServer/Models/ForbiddenUploadEntry.cs rename to MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs index cf3e4ae..a558423 100644 --- a/MareSynchronosServer/MareSynchronosServer/Models/ForbiddenUploadEntry.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosServer.Models +namespace MareSynchronosShared.Models { public class ForbiddenUploadEntry { diff --git a/MareSynchronosServer/MareSynchronosServer/Models/LodeStoneAuth.cs b/MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs similarity index 78% rename from MareSynchronosServer/MareSynchronosServer/Models/LodeStoneAuth.cs rename to MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs index 976a52d..38448de 100644 --- a/MareSynchronosServer/MareSynchronosServer/Models/LodeStoneAuth.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs @@ -1,7 +1,6 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; -namespace MareSynchronosServer.Models +namespace MareSynchronosShared.Models { public class LodeStoneAuth { diff --git a/MareSynchronosServer/MareSynchronosServer/Models/User.cs b/MareSynchronosServer/MareSynchronosShared/Models/User.cs similarity index 81% rename from MareSynchronosServer/MareSynchronosServer/Models/User.cs rename to MareSynchronosServer/MareSynchronosShared/Models/User.cs index ff4fcd2..2e5a31f 100644 --- a/MareSynchronosServer/MareSynchronosServer/Models/User.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/User.cs @@ -1,7 +1,6 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; -namespace MareSynchronosServer.Models +namespace MareSynchronosShared.Models { public class User { 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" +}