2
									
								
								MareAPI
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								MareAPI
									
									
									
									
									
								
							 Submodule MareAPI updated: 714d990c0b...50a447c4d0
									
								
							| @@ -7,6 +7,8 @@ using System.Text.Encodings.Web; | ||||
| using System.Threading.Tasks; | ||||
| using MareSynchronosServer.Data; | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.Extensions.Options; | ||||
| @@ -20,8 +22,13 @@ namespace MareSynchronosServer.Authentication | ||||
|  | ||||
|         protected override async Task<AuthenticateResult> HandleAuthenticateAsync() | ||||
|         { | ||||
|             if (!Request.Headers.ContainsKey("Authorization")) | ||||
|             var endpoint = Context.GetEndpoint(); | ||||
|             var endpointMetaData = endpoint?.Metadata?.GetMetadata<IAllowAnonymous>(); | ||||
|  | ||||
|             if (!Request.Headers.ContainsKey("Authorization") && endpointMetaData == null) | ||||
|                 return AuthenticateResult.Fail("Failed Authorization"); | ||||
|             else if (!Request.Headers.ContainsKey("Authorization") && endpointMetaData != null) | ||||
|                 return AuthenticateResult.NoResult(); | ||||
|  | ||||
|             var authHeader = Request.Headers["Authorization"].ToString(); | ||||
|  | ||||
| @@ -37,6 +44,10 @@ namespace MareSynchronosServer.Authentication | ||||
|             { | ||||
|                 return AuthenticateResult.Fail("Failed Authorization"); | ||||
|             } | ||||
|             else if (endpointMetaData != null && uid == null) | ||||
|             { | ||||
|                 return AuthenticateResult.NoResult(); | ||||
|             } | ||||
|  | ||||
|             var claims = new List<Claim> { | ||||
|                 new Claim(ClaimTypes.NameIdentifier, uid) | ||||
|   | ||||
| @@ -15,6 +15,7 @@ namespace MareSynchronosServer.Data | ||||
|         public DbSet<ForbiddenUploadEntry> ForbiddenUploadEntries { get; set; } | ||||
|         public DbSet<Banned> BannedUsers { get; set; } | ||||
|         public DbSet<Auth> Auth { get; set; } | ||||
|         public DbSet<LodeStoneAuth> LodeStoneAuth { get; set; } | ||||
|  | ||||
|  | ||||
|         protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||
| @@ -30,6 +31,7 @@ namespace MareSynchronosServer.Data | ||||
|             modelBuilder.Entity<ClientPair>().HasIndex(c => c.OtherUserUID); | ||||
|             modelBuilder.Entity<ForbiddenUploadEntry>().ToTable("forbidden_upload_entries"); | ||||
|             modelBuilder.Entity<Banned>().ToTable("banned_users"); | ||||
|             modelBuilder.Entity<LodeStoneAuth>().ToTable("lodestone_auth"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										331
									
								
								MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,331 @@ | ||||
| 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<DiscordBot> logger; | ||||
|         private string authToken = string.Empty; | ||||
|         DiscordSocketClient discordClient; | ||||
|         ConcurrentDictionary<ulong, string> DiscordLodestoneMapping = new(); | ||||
|         private Timer _timer; | ||||
|         private readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "na" }; | ||||
|  | ||||
|         public DiscordBot(IServiceProvider services, IConfiguration configuration, ILogger<DiscordBot> logger) | ||||
|         { | ||||
|             this.services = services; | ||||
|             this.configuration = configuration; | ||||
|             this.logger = logger; | ||||
|  | ||||
|             discordClient = new(new DiscordSocketConfig() | ||||
|             { | ||||
|                 DefaultRetryMode = RetryMode.AlwaysRetry | ||||
|             }); | ||||
|  | ||||
|             discordClient.Log += Log; | ||||
|         } | ||||
|  | ||||
|         private async Task DiscordClient_SlashCommandExecuted(SocketSlashCommand arg) | ||||
|         { | ||||
|             if (arg.Data.Name == "register") | ||||
|             { | ||||
|                 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/<CHARACTERID>/", required: true); | ||||
|                 await arg.RespondWithModalAsync(modal.Build()); | ||||
|             } | ||||
|             else if (arg.Data.Name == "verify") | ||||
|             { | ||||
|                 await arg.DeferAsync(true); | ||||
|                 Embed response = await HandleVerifyAsync(arg.User.Id); | ||||
|                 await arg.FollowupAsync(embeds: new[] { response }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private async Task DiscordClient_ModalSubmitted(SocketModal arg) | ||||
|         { | ||||
|             if (arg.Data.CustomId == "register_modal") | ||||
|             { | ||||
|                 await arg.DeferAsync(true); | ||||
|                 var embed = await HandleRegisterModalAsync(arg); | ||||
|                 await arg.FollowupAsync(embeds: new Embed[] { embed }, ephemeral: true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private async Task<Embed> HandleVerifyAsync(ulong id) | ||||
|         { | ||||
|             var embedBuilder = new EmbedBuilder(); | ||||
|             if (!DiscordLodestoneMapping.ContainsKey(id)) | ||||
|             { | ||||
|                 embedBuilder.WithTitle("Cannot verify registration"); | ||||
|                 embedBuilder.WithDescription("You need to **/register** first before you can **/verify**"); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 using var scope = services.CreateScope(); | ||||
|                 var req = new HttpClient(); | ||||
|                 using var db = scope.ServiceProvider.GetService<MareDbContext>(); | ||||
|  | ||||
|                 var lodestoneAuth = db.LodeStoneAuth.SingleOrDefault(u => u.DiscordId == id); | ||||
|                 if (lodestoneAuth != null) | ||||
|                 { | ||||
|                     Random rand = new(); | ||||
|                     var randomServer = LodestoneServers[rand.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<bool>("PurgeUnusedAccounts")) | ||||
|                             { | ||||
|                                 var purgedDays = configuration.GetValue<int>("PurgeUnusedAccountsPeriodInDays"); | ||||
|                                 user.LastLoggedIn = DateTime.UtcNow - TimeSpan.FromDays(purgedDays) + TimeSpan.FromHours(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. Start over with **/register**"); | ||||
|                         } | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         embedBuilder.WithTitle("Failed to get response from Lodestone"); | ||||
|                         embedBuilder.WithDescription("Try again later"); | ||||
|                         lodestoneAuth.StartedAt = DateTime.UtcNow; | ||||
|                     } | ||||
|  | ||||
|                     await db.SaveChangesAsync(); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     embedBuilder.WithTitle("Your auth has expired"); | ||||
|                     embedBuilder.WithDescription("Start again with **/register**"); | ||||
|                     DiscordLodestoneMapping.TryRemove(id, out _); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return embedBuilder.Build(); | ||||
|         } | ||||
|  | ||||
|         private async Task<Embed> 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("-", ""); | ||||
|  | ||||
|                 var db = scope.ServiceProvider.GetService<MareDbContext>(); | ||||
|  | ||||
|                 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 | ||||
|                         + "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<string> GenerateLodestoneAuth(ulong discordid, string hashedLodestoneId, MareDbContext dbContext) | ||||
|         { | ||||
|             var auth = MareHub.GenerateRandomString(64); | ||||
|             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 isLodestoneUrl = Regex.Match(lodestoneUrl, @"https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/\d+").Success; | ||||
|             if (!isLodestoneUrl) return null; | ||||
|  | ||||
|             lodestoneUrl = lodestoneUrl.Split('/', StringSplitOptions.RemoveEmptyEntries).Last(); | ||||
|             if (!int.TryParse(lodestoneUrl, out int lodestoneId)) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             return lodestoneId; | ||||
|         } | ||||
|  | ||||
|         private async Task DiscordClient_Ready() | ||||
|         { | ||||
|             var cb = new SlashCommandBuilder(); | ||||
|             cb.WithName("register"); | ||||
|             cb.WithDescription("Starts the registration process for the Mare Synchronos server of this Discord"); | ||||
|  | ||||
|             var cb2 = new SlashCommandBuilder(); | ||||
|             cb2.WithName("verify"); | ||||
|             cb2.WithDescription("Finishes the registration process for the Mare Synchronos server of this Discord"); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 await discordClient.CreateGlobalApplicationCommandAsync(cb.Build()); | ||||
|                 await discordClient.CreateGlobalApplicationCommandAsync(cb2.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) | ||||
|         { | ||||
|             authToken = configuration.GetValue<string>("DiscordBotToken"); | ||||
|             if (!string.IsNullOrEmpty(authToken)) | ||||
|             { | ||||
|                 await discordClient.LoginAsync(TokenType.Bot, authToken); | ||||
|                 await discordClient.StartAsync(); | ||||
|  | ||||
|                 discordClient.Ready += DiscordClient_Ready; | ||||
|                 discordClient.SlashCommandExecuted += DiscordClient_SlashCommandExecuted; | ||||
|                 discordClient.ModalSubmitted += DiscordClient_ModalSubmitted; | ||||
|  | ||||
|                 _timer = new Timer(UpdateStatus, null, TimeSpan.Zero, TimeSpan.FromSeconds(15)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void UpdateStatus(object state) | ||||
|         { | ||||
|             using var scope = services.CreateScope(); | ||||
|             var db = scope.ServiceProvider.GetService<MareDbContext>(); | ||||
|  | ||||
|             var users = db.Users.Count(c => c.CharacterIdentification != null); | ||||
|  | ||||
|             discordClient.SetActivityAsync(new Game("Mare for " + users + " Users")); | ||||
|         } | ||||
|  | ||||
|         public async Task StopAsync(CancellationToken cancellationToken) | ||||
|         { | ||||
|             _timer?.Change(Timeout.Infinite, 0); | ||||
|  | ||||
|             await discordClient.LogoutAsync(); | ||||
|             await discordClient.StopAsync(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -73,6 +73,20 @@ namespace MareSynchronosServer | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 var lodestoneAuths = dbContext.LodeStoneAuth.Include(u => u.User).Where(a => a.StartedAt != null).ToList(); | ||||
|                 List<LodeStoneAuth> expiredAuths = new List<LodeStoneAuth>(); | ||||
|                 foreach (var auth in lodestoneAuths) | ||||
|                 { | ||||
|                     if (auth.StartedAt < DateTime.UtcNow - TimeSpan.FromMinutes(15)) | ||||
|                     { | ||||
|                         expiredAuths.Add(auth); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 dbContext.RemoveRange(expiredAuths); | ||||
|                 dbContext.RemoveRange(expiredAuths.Select(a => a.User)); | ||||
|  | ||||
|  | ||||
|                 if (!bool.TryParse(_configuration["PurgeUnusedAccounts"], out var purgeUnusedAccounts)) | ||||
|                 { | ||||
|                     purgeUnusedAccounts = false; | ||||
| @@ -91,7 +105,7 @@ namespace MareSynchronosServer | ||||
|                     List<User> usersToRemove = new(); | ||||
|                     foreach (var user in allUsers) | ||||
|                     { | ||||
|                         if (user.LastLoggedIn < (DateTime.Now - TimeSpan.FromDays(usersOlderThanDays))) | ||||
|                         if (user.LastLoggedIn < (DateTime.UtcNow - TimeSpan.FromDays(usersOlderThanDays))) | ||||
|                         { | ||||
|                             _logger.LogInformation("User outdated: " + user.UID); | ||||
|                             usersToRemove.Add(user); | ||||
| @@ -100,6 +114,13 @@ namespace MareSynchronosServer | ||||
|  | ||||
|                     foreach (var user in usersToRemove) | ||||
|                     { | ||||
|                         var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID); | ||||
|  | ||||
|                         if (lodestone != null) | ||||
|                         { | ||||
|                             dbContext.Remove(lodestone); | ||||
|                         } | ||||
|  | ||||
|                         var auth = dbContext.Auth.Single(a => a.UserUID == user.UID); | ||||
|  | ||||
|                         var userFiles = dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == user.UID).ToList(); | ||||
|   | ||||
| @@ -1,8 +1,5 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using MareSynchronos.API; | ||||
| using MareSynchronosServer.Authentication; | ||||
| @@ -28,6 +25,13 @@ namespace MareSynchronosServer.Hubs | ||||
|             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); | ||||
|  | ||||
|             if (lodestone != null) | ||||
|             { | ||||
|                 _dbContext.Remove(lodestone); | ||||
|             } | ||||
|  | ||||
|             while (_dbContext.Files.Any(f => f.Uploader == userEntry)) | ||||
|             { | ||||
|                 await Task.Delay(1000); | ||||
| @@ -82,12 +86,6 @@ namespace MareSynchronosServer.Hubs | ||||
|             return otherEntries.Select(e => e.User.CharacterIdentification).Distinct().ToList(); | ||||
|         } | ||||
|  | ||||
|         [HubMethodName(Api.InvokeUserGetOnlineUsers)] | ||||
|         public async Task<int> GetOnlineUsers() | ||||
|         { | ||||
|             return await _dbContext.Users.CountAsync(u => !string.IsNullOrEmpty(u.CharacterIdentification)); | ||||
|         } | ||||
|  | ||||
|         [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] | ||||
|         [HubMethodName(Api.InvokeUserGetPairedClients)] | ||||
|         public async Task<List<ClientPairDto>> GetPairedClients() | ||||
| @@ -139,47 +137,6 @@ namespace MareSynchronosServer.Hubs | ||||
|             MareMetrics.UserPushDataTo.Inc(visibleCharacterIds.Count); | ||||
|         } | ||||
|  | ||||
|         [HubMethodName(Api.InvokeUserRegister)] | ||||
|         public async Task<string> Register() | ||||
|         { | ||||
|             using var sha256 = SHA256.Create(); | ||||
|             var user = new User(); | ||||
|  | ||||
|             var hasValidUid = false; | ||||
|             while (!hasValidUid) | ||||
|             { | ||||
|                 var uid = GenerateRandomString(10); | ||||
|                 if (_dbContext.Users.Any(u => u.UID == uid)) continue; | ||||
|                 user.UID = uid; | ||||
|                 hasValidUid = true; | ||||
|             } | ||||
|  | ||||
|             // make the first registered user on the service to admin | ||||
|             if (!await _dbContext.Users.AnyAsync()) | ||||
|             { | ||||
|                 user.IsAdmin = true; | ||||
|             } | ||||
|  | ||||
|             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 | ||||
|             }; | ||||
|  | ||||
|             _dbContext.Users.Add(user); | ||||
|             _dbContext.Auth.Add(auth); | ||||
|  | ||||
|             _logger.LogInformation("User registered: " + user.UID); | ||||
|  | ||||
|             MareMetrics.UsersRegistered.Inc(); | ||||
|  | ||||
|             await _dbContext.SaveChangesAsync(); | ||||
|             return computedHash; | ||||
|         } | ||||
|  | ||||
|  | ||||
|         [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] | ||||
|         [HubMethodName(Api.SendUserPairedClientAddition)] | ||||
|         public async Task SendPairedClientAddition(string uid) | ||||
|   | ||||
| @@ -4,8 +4,10 @@ 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 Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http.Features; | ||||
| using Microsoft.AspNetCore.SignalR; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -14,6 +16,8 @@ using Microsoft.Extensions.Logging; | ||||
|  | ||||
| namespace MareSynchronosServer.Hubs | ||||
| { | ||||
|     [AllowAnonymous] | ||||
|     [Authorize(AuthenticationSchemes = SecretKeyAuthenticationHandler.AuthScheme)] | ||||
|     public partial class MareHub : Hub | ||||
|     { | ||||
|         private readonly SystemInfoService _systemInfoService; | ||||
| @@ -29,6 +33,7 @@ namespace MareSynchronosServer.Hubs | ||||
|             _dbContext = mareDbContext; | ||||
|         } | ||||
|  | ||||
|         [AllowAnonymous] | ||||
|         [HubMethodName(Api.InvokeHeartbeat)] | ||||
|         public async Task<ConnectionDto> Heartbeat(string? characterIdentification) | ||||
|         { | ||||
| @@ -44,7 +49,6 @@ namespace MareSynchronosServer.Hubs | ||||
|  | ||||
|             if (!string.IsNullOrEmpty(userId) && !isBanned && !string.IsNullOrEmpty(characterIdentification)) | ||||
|             { | ||||
|                 _logger.LogInformation("Connection from " + userId); | ||||
|                 var user = (await _dbContext.Users.SingleAsync(u => u.UID == userId)); | ||||
|                 if (!string.IsNullOrEmpty(user.CharacterIdentification) && characterIdentification != user.CharacterIdentification) | ||||
|                 { | ||||
| @@ -77,11 +81,13 @@ namespace MareSynchronosServer.Hubs | ||||
|         } | ||||
|  | ||||
|         [HubMethodName(Api.InvokeGetSystemInfo)] | ||||
|         [AllowAnonymous] | ||||
|         public async Task<SystemInfoDto> GetSystemInfo() | ||||
|         { | ||||
|             return _systemInfoService.SystemInfoDto; | ||||
|         } | ||||
|  | ||||
|         [AllowAnonymous] | ||||
|         public override Task OnConnectedAsync() | ||||
|         { | ||||
|             var feature = Context.Features.Get<IHttpConnectionFeature>(); | ||||
| @@ -90,6 +96,7 @@ namespace MareSynchronosServer.Hubs | ||||
|             return base.OnConnectedAsync(); | ||||
|         } | ||||
|  | ||||
|         [AllowAnonymous] | ||||
|         public override async Task OnDisconnectedAsync(Exception exception) | ||||
|         { | ||||
|             MareMetrics.Connections.Dec(); | ||||
|   | ||||
| @@ -7,7 +7,9 @@ | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="AspNetCoreRateLimit" Version="4.0.2" /> | ||||
|     <PackageReference Include="Bazinga.AspNetCore.Authentication.Basic" Version="2.0.1" /> | ||||
|     <PackageReference Include="Discord.Net" Version="3.7.2" /> | ||||
|     <PackageReference Include="EFCore.NamingConventions" Version="6.0.0" /> | ||||
|     <PackageReference Include="Karambolo.Extensions.Logging.File" Version="3.3.0" /> | ||||
|     <PackageReference Include="lz4net" Version="1.0.15.93" /> | ||||
|   | ||||
							
								
								
									
										283
									
								
								MareSynchronosServer/MareSynchronosServer/Migrations/20220801121419_AddLodestoneAuth.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								MareSynchronosServer/MareSynchronosServer/Migrations/20220801121419_AddLodestoneAuth.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,283 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using MareSynchronosServer.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace MareSynchronosServer.Migrations | ||||
| { | ||||
|     [DbContext(typeof(MareDbContext))] | ||||
|     [Migration("20220801121419_AddLodestoneAuth")] | ||||
|     partial class AddLodestoneAuth | ||||
|     { | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "6.0.6") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => | ||||
|                 { | ||||
|                     b.Property<string>("HashedKey") | ||||
|                         .HasMaxLength(64) | ||||
|                         .HasColumnType("character varying(64)") | ||||
|                         .HasColumnName("hashed_key"); | ||||
|  | ||||
|                     b.Property<string>("UserUID") | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("user_uid"); | ||||
|  | ||||
|                     b.HasKey("HashedKey") | ||||
|                         .HasName("pk_auth"); | ||||
|  | ||||
|                     b.HasIndex("UserUID") | ||||
|                         .HasDatabaseName("ix_auth_user_uid"); | ||||
|  | ||||
|                     b.ToTable("auth", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.Banned", b => | ||||
|                 { | ||||
|                     b.Property<string>("CharacterIdentification") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("character_identification"); | ||||
|  | ||||
|                     b.Property<string>("Reason") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("reason"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.HasKey("CharacterIdentification") | ||||
|                         .HasName("pk_banned_users"); | ||||
|  | ||||
|                     b.ToTable("banned_users", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => | ||||
|                 { | ||||
|                     b.Property<string>("UserUID") | ||||
|                         .HasMaxLength(10) | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("user_uid"); | ||||
|  | ||||
|                     b.Property<string>("OtherUserUID") | ||||
|                         .HasMaxLength(10) | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("other_user_uid"); | ||||
|  | ||||
|                     b.Property<bool>("AllowReceivingMessages") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("allow_receiving_messages"); | ||||
|  | ||||
|                     b.Property<bool>("IsPaused") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_paused"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.HasKey("UserUID", "OtherUserUID") | ||||
|                         .HasName("pk_client_pairs"); | ||||
|  | ||||
|                     b.HasIndex("OtherUserUID") | ||||
|                         .HasDatabaseName("ix_client_pairs_other_user_uid"); | ||||
|  | ||||
|                     b.HasIndex("UserUID") | ||||
|                         .HasDatabaseName("ix_client_pairs_user_uid"); | ||||
|  | ||||
|                     b.ToTable("client_pairs", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => | ||||
|                 { | ||||
|                     b.Property<string>("Hash") | ||||
|                         .HasMaxLength(40) | ||||
|                         .HasColumnType("character varying(40)") | ||||
|                         .HasColumnName("hash"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.Property<bool>("Uploaded") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("uploaded"); | ||||
|  | ||||
|                     b.Property<string>("UploaderUID") | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("uploader_uid"); | ||||
|  | ||||
|                     b.HasKey("Hash") | ||||
|                         .HasName("pk_file_caches"); | ||||
|  | ||||
|                     b.HasIndex("UploaderUID") | ||||
|                         .HasDatabaseName("ix_file_caches_uploader_uid"); | ||||
|  | ||||
|                     b.ToTable("file_caches", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.ForbiddenUploadEntry", b => | ||||
|                 { | ||||
|                     b.Property<string>("Hash") | ||||
|                         .HasMaxLength(40) | ||||
|                         .HasColumnType("character varying(40)") | ||||
|                         .HasColumnName("hash"); | ||||
|  | ||||
|                     b.Property<string>("ForbiddenBy") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("forbidden_by"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.HasKey("Hash") | ||||
|                         .HasName("pk_forbidden_upload_entries"); | ||||
|  | ||||
|                     b.ToTable("forbidden_upload_entries", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => | ||||
|                 { | ||||
|                     b.Property<decimal>("DiscordId") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("numeric(20,0)") | ||||
|                         .HasColumnName("discord_id"); | ||||
|  | ||||
|                     b.Property<string>("HashedLodestoneId") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("hashed_lodestone_id"); | ||||
|  | ||||
|                     b.Property<string>("LodestoneAuthString") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("lodestone_auth_string"); | ||||
|  | ||||
|                     b.Property<DateTime>("StartedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("started_at"); | ||||
|  | ||||
|                     b.Property<string>("UserUID") | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("user_uid"); | ||||
|  | ||||
|                     b.HasKey("DiscordId") | ||||
|                         .HasName("pk_lodestone_auth"); | ||||
|  | ||||
|                     b.HasIndex("UserUID") | ||||
|                         .HasDatabaseName("ix_lodestone_auth_user_uid"); | ||||
|  | ||||
|                     b.ToTable("lodestone_auth", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.User", b => | ||||
|                 { | ||||
|                     b.Property<string>("UID") | ||||
|                         .HasMaxLength(10) | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("uid"); | ||||
|  | ||||
|                     b.Property<string>("CharacterIdentification") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("character_identification"); | ||||
|  | ||||
|                     b.Property<bool>("IsAdmin") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_admin"); | ||||
|  | ||||
|                     b.Property<bool>("IsModerator") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_moderator"); | ||||
|  | ||||
|                     b.Property<DateTime>("LastLoggedIn") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("last_logged_in"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.HasKey("UID") | ||||
|                         .HasName("pk_users"); | ||||
|  | ||||
|                     b.HasIndex("CharacterIdentification") | ||||
|                         .HasDatabaseName("ix_users_character_identification"); | ||||
|  | ||||
|                     b.ToTable("users", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => | ||||
|                 { | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "User") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserUID") | ||||
|                         .HasConstraintName("fk_auth_users_user_temp_id"); | ||||
|  | ||||
|                     b.Navigation("User"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => | ||||
|                 { | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "OtherUser") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("OtherUserUID") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); | ||||
|  | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "User") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserUID") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_client_pairs_users_user_temp_id2"); | ||||
|  | ||||
|                     b.Navigation("OtherUser"); | ||||
|  | ||||
|                     b.Navigation("User"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => | ||||
|                 { | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "Uploader") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UploaderUID") | ||||
|                         .HasConstraintName("fk_file_caches_users_uploader_uid"); | ||||
|  | ||||
|                     b.Navigation("Uploader"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => | ||||
|                 { | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "User") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserUID") | ||||
|                         .HasConstraintName("fk_lodestone_auth_users_user_uid"); | ||||
|  | ||||
|                     b.Navigation("User"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,44 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace MareSynchronosServer.Migrations | ||||
| { | ||||
|     public partial class AddLodestoneAuth : Migration | ||||
|     { | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "lodestone_auth", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     discord_id = table.Column<decimal>(type: "numeric(20,0)", nullable: false), | ||||
|                     hashed_lodestone_id = table.Column<string>(type: "text", nullable: true), | ||||
|                     lodestone_auth_string = table.Column<string>(type: "text", nullable: true), | ||||
|                     user_uid = table.Column<string>(type: "character varying(10)", nullable: true), | ||||
|                     started_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_lodestone_auth", x => x.discord_id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_lodestone_auth_users_user_uid", | ||||
|                         column: x => x.user_uid, | ||||
|                         principalTable: "users", | ||||
|                         principalColumn: "uid"); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_lodestone_auth_user_uid", | ||||
|                 table: "lodestone_auth", | ||||
|                 column: "user_uid"); | ||||
|         } | ||||
|  | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "lodestone_auth"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,283 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using MareSynchronosServer.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace MareSynchronosServer.Migrations | ||||
| { | ||||
|     [DbContext(typeof(MareDbContext))] | ||||
|     [Migration("20220801122103_AddNullableLodestoneAuthProperties")] | ||||
|     partial class AddNullableLodestoneAuthProperties | ||||
|     { | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "6.0.6") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => | ||||
|                 { | ||||
|                     b.Property<string>("HashedKey") | ||||
|                         .HasMaxLength(64) | ||||
|                         .HasColumnType("character varying(64)") | ||||
|                         .HasColumnName("hashed_key"); | ||||
|  | ||||
|                     b.Property<string>("UserUID") | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("user_uid"); | ||||
|  | ||||
|                     b.HasKey("HashedKey") | ||||
|                         .HasName("pk_auth"); | ||||
|  | ||||
|                     b.HasIndex("UserUID") | ||||
|                         .HasDatabaseName("ix_auth_user_uid"); | ||||
|  | ||||
|                     b.ToTable("auth", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.Banned", b => | ||||
|                 { | ||||
|                     b.Property<string>("CharacterIdentification") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("character_identification"); | ||||
|  | ||||
|                     b.Property<string>("Reason") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("reason"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.HasKey("CharacterIdentification") | ||||
|                         .HasName("pk_banned_users"); | ||||
|  | ||||
|                     b.ToTable("banned_users", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => | ||||
|                 { | ||||
|                     b.Property<string>("UserUID") | ||||
|                         .HasMaxLength(10) | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("user_uid"); | ||||
|  | ||||
|                     b.Property<string>("OtherUserUID") | ||||
|                         .HasMaxLength(10) | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("other_user_uid"); | ||||
|  | ||||
|                     b.Property<bool>("AllowReceivingMessages") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("allow_receiving_messages"); | ||||
|  | ||||
|                     b.Property<bool>("IsPaused") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_paused"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.HasKey("UserUID", "OtherUserUID") | ||||
|                         .HasName("pk_client_pairs"); | ||||
|  | ||||
|                     b.HasIndex("OtherUserUID") | ||||
|                         .HasDatabaseName("ix_client_pairs_other_user_uid"); | ||||
|  | ||||
|                     b.HasIndex("UserUID") | ||||
|                         .HasDatabaseName("ix_client_pairs_user_uid"); | ||||
|  | ||||
|                     b.ToTable("client_pairs", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => | ||||
|                 { | ||||
|                     b.Property<string>("Hash") | ||||
|                         .HasMaxLength(40) | ||||
|                         .HasColumnType("character varying(40)") | ||||
|                         .HasColumnName("hash"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.Property<bool>("Uploaded") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("uploaded"); | ||||
|  | ||||
|                     b.Property<string>("UploaderUID") | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("uploader_uid"); | ||||
|  | ||||
|                     b.HasKey("Hash") | ||||
|                         .HasName("pk_file_caches"); | ||||
|  | ||||
|                     b.HasIndex("UploaderUID") | ||||
|                         .HasDatabaseName("ix_file_caches_uploader_uid"); | ||||
|  | ||||
|                     b.ToTable("file_caches", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.ForbiddenUploadEntry", b => | ||||
|                 { | ||||
|                     b.Property<string>("Hash") | ||||
|                         .HasMaxLength(40) | ||||
|                         .HasColumnType("character varying(40)") | ||||
|                         .HasColumnName("hash"); | ||||
|  | ||||
|                     b.Property<string>("ForbiddenBy") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("forbidden_by"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.HasKey("Hash") | ||||
|                         .HasName("pk_forbidden_upload_entries"); | ||||
|  | ||||
|                     b.ToTable("forbidden_upload_entries", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => | ||||
|                 { | ||||
|                     b.Property<decimal>("DiscordId") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("numeric(20,0)") | ||||
|                         .HasColumnName("discord_id"); | ||||
|  | ||||
|                     b.Property<string>("HashedLodestoneId") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("hashed_lodestone_id"); | ||||
|  | ||||
|                     b.Property<string>("LodestoneAuthString") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("lodestone_auth_string"); | ||||
|  | ||||
|                     b.Property<DateTime?>("StartedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("started_at"); | ||||
|  | ||||
|                     b.Property<string>("UserUID") | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("user_uid"); | ||||
|  | ||||
|                     b.HasKey("DiscordId") | ||||
|                         .HasName("pk_lodestone_auth"); | ||||
|  | ||||
|                     b.HasIndex("UserUID") | ||||
|                         .HasDatabaseName("ix_lodestone_auth_user_uid"); | ||||
|  | ||||
|                     b.ToTable("lodestone_auth", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.User", b => | ||||
|                 { | ||||
|                     b.Property<string>("UID") | ||||
|                         .HasMaxLength(10) | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("uid"); | ||||
|  | ||||
|                     b.Property<string>("CharacterIdentification") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("character_identification"); | ||||
|  | ||||
|                     b.Property<bool>("IsAdmin") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_admin"); | ||||
|  | ||||
|                     b.Property<bool>("IsModerator") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_moderator"); | ||||
|  | ||||
|                     b.Property<DateTime>("LastLoggedIn") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("last_logged_in"); | ||||
|  | ||||
|                     b.Property<byte[]>("Timestamp") | ||||
|                         .IsConcurrencyToken() | ||||
|                         .ValueGeneratedOnAddOrUpdate() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("timestamp"); | ||||
|  | ||||
|                     b.HasKey("UID") | ||||
|                         .HasName("pk_users"); | ||||
|  | ||||
|                     b.HasIndex("CharacterIdentification") | ||||
|                         .HasDatabaseName("ix_users_character_identification"); | ||||
|  | ||||
|                     b.ToTable("users", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.Auth", b => | ||||
|                 { | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "User") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserUID") | ||||
|                         .HasConstraintName("fk_auth_users_user_temp_id"); | ||||
|  | ||||
|                     b.Navigation("User"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => | ||||
|                 { | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "OtherUser") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("OtherUserUID") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); | ||||
|  | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "User") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserUID") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_client_pairs_users_user_temp_id2"); | ||||
|  | ||||
|                     b.Navigation("OtherUser"); | ||||
|  | ||||
|                     b.Navigation("User"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.FileCache", b => | ||||
|                 { | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "Uploader") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UploaderUID") | ||||
|                         .HasConstraintName("fk_file_caches_users_uploader_uid"); | ||||
|  | ||||
|                     b.Navigation("Uploader"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => | ||||
|                 { | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "User") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserUID") | ||||
|                         .HasConstraintName("fk_lodestone_auth_users_user_uid"); | ||||
|  | ||||
|                     b.Navigation("User"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,34 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace MareSynchronosServer.Migrations | ||||
| { | ||||
|     public partial class AddNullableLodestoneAuthProperties : Migration | ||||
|     { | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterColumn<DateTime>( | ||||
|                 name: "started_at", | ||||
|                 table: "lodestone_auth", | ||||
|                 type: "timestamp with time zone", | ||||
|                 nullable: true, | ||||
|                 oldClrType: typeof(DateTime), | ||||
|                 oldType: "timestamp with time zone"); | ||||
|         } | ||||
|  | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterColumn<DateTime>( | ||||
|                 name: "started_at", | ||||
|                 table: "lodestone_auth", | ||||
|                 type: "timestamp with time zone", | ||||
|                 nullable: false, | ||||
|                 defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), | ||||
|                 oldClrType: typeof(DateTime), | ||||
|                 oldType: "timestamp with time zone", | ||||
|                 oldNullable: true); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -155,6 +155,38 @@ namespace MareSynchronosServer.Migrations | ||||
|                     b.ToTable("forbidden_upload_entries", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => | ||||
|                 { | ||||
|                     b.Property<decimal>("DiscordId") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("numeric(20,0)") | ||||
|                         .HasColumnName("discord_id"); | ||||
|  | ||||
|                     b.Property<string>("HashedLodestoneId") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("hashed_lodestone_id"); | ||||
|  | ||||
|                     b.Property<string>("LodestoneAuthString") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("lodestone_auth_string"); | ||||
|  | ||||
|                     b.Property<DateTime?>("StartedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("started_at"); | ||||
|  | ||||
|                     b.Property<string>("UserUID") | ||||
|                         .HasColumnType("character varying(10)") | ||||
|                         .HasColumnName("user_uid"); | ||||
|  | ||||
|                     b.HasKey("DiscordId") | ||||
|                         .HasName("pk_lodestone_auth"); | ||||
|  | ||||
|                     b.HasIndex("UserUID") | ||||
|                         .HasDatabaseName("ix_lodestone_auth_user_uid"); | ||||
|  | ||||
|                     b.ToTable("lodestone_auth", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.User", b => | ||||
|                 { | ||||
|                     b.Property<string>("UID") | ||||
| @@ -233,6 +265,16 @@ namespace MareSynchronosServer.Migrations | ||||
|  | ||||
|                     b.Navigation("Uploader"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("MareSynchronosServer.Models.LodeStoneAuth", b => | ||||
|                 { | ||||
|                     b.HasOne("MareSynchronosServer.Models.User", "User") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("UserUID") | ||||
|                         .HasConstraintName("fk_lodestone_auth_users_user_uid"); | ||||
|  | ||||
|                     b.Navigation("User"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,15 @@ | ||||
| using System; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
|  | ||||
| namespace MareSynchronosServer.Models | ||||
| { | ||||
|     public class LodeStoneAuth | ||||
|     { | ||||
|         [Key] | ||||
|         public ulong DiscordId { get; set; } | ||||
|         public string HashedLodestoneId { get; set; } | ||||
|         public string? LodestoneAuthString { get; set; } | ||||
|         public User? User { get; set; } | ||||
|         public DateTime? StartedAt { get; set; } | ||||
|     } | ||||
| } | ||||
| @@ -37,6 +37,8 @@ namespace MareSynchronosServer | ||||
|                     user.CharacterIdentification = null; | ||||
|                 } | ||||
|                 var looseFiles = context.Files.Where(f => f.Uploaded == false); | ||||
|                 var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null); | ||||
|                 context.RemoveRange(unfinishedRegistrations); | ||||
|                 context.RemoveRange(looseFiles); | ||||
|                 context.SaveChanges(); | ||||
|  | ||||
|   | ||||
| @@ -15,8 +15,9 @@ using Microsoft.AspNetCore.SignalR; | ||||
| using Prometheus; | ||||
| using WebSocketOptions = Microsoft.AspNetCore.Builder.WebSocketOptions; | ||||
| using Microsoft.Extensions.FileProviders; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using MareSynchronosServer.Discord; | ||||
| using AspNetCoreRateLimit; | ||||
|  | ||||
| namespace MareSynchronosServer | ||||
| { | ||||
| @@ -42,6 +43,13 @@ namespace MareSynchronosServer | ||||
|                 hubOptions.StreamBufferCapacity = 200; | ||||
|             }); | ||||
|  | ||||
|             services.AddMemoryCache(); | ||||
|  | ||||
|             services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting")); | ||||
|             services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies")); | ||||
|  | ||||
|             services.AddInMemoryRateLimiting(); | ||||
|  | ||||
|             services.AddSingleton<SystemInfoService, SystemInfoService>(); | ||||
|             services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>(); | ||||
|             services.AddTransient(_ => Configuration); | ||||
| @@ -56,6 +64,7 @@ namespace MareSynchronosServer | ||||
|  | ||||
|             services.AddHostedService<FileCleanupService>(); | ||||
|             services.AddHostedService(provider => provider.GetService<SystemInfoService>()); | ||||
|             services.AddHostedService<DiscordBot>(); | ||||
|  | ||||
|             services.AddDatabaseDeveloperPageExceptionFilter(); | ||||
|             services.AddAuthentication(options => | ||||
| @@ -63,6 +72,9 @@ namespace MareSynchronosServer | ||||
|                     options.DefaultScheme = SecretKeyAuthenticationHandler.AuthScheme; | ||||
|                 }) | ||||
|                 .AddScheme<AuthenticationSchemeOptions, SecretKeyAuthenticationHandler>(SecretKeyAuthenticationHandler.AuthScheme, options => { }); | ||||
|             services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); | ||||
|  | ||||
|             services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>(); | ||||
|         } | ||||
|  | ||||
|         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | ||||
| @@ -81,6 +93,8 @@ namespace MareSynchronosServer | ||||
|                 app.UseHsts(); | ||||
|             } | ||||
|  | ||||
|             app.UseIpRateLimiting(); | ||||
|  | ||||
|             app.UseStaticFiles(); | ||||
|             app.UseHttpLogging(); | ||||
|  | ||||
| @@ -90,19 +104,18 @@ namespace MareSynchronosServer | ||||
|                 KeepAliveInterval = TimeSpan.FromSeconds(10), | ||||
|             }; | ||||
|  | ||||
|             app.UseStaticFiles(new StaticFileOptions() | ||||
|             { | ||||
|                 FileProvider = new PhysicalFileProvider(Configuration["CacheDirectory"]), | ||||
|                 RequestPath = "/cache", | ||||
|                 ServeUnknownFileTypes = true | ||||
|             }); | ||||
|  | ||||
|             app.UseHttpMetrics(); | ||||
|             app.UseWebSockets(webSocketOptions); | ||||
|  | ||||
|             app.UseAuthentication(); | ||||
|             app.UseAuthorization(); | ||||
|  | ||||
|             app.UseStaticFiles(new StaticFileOptions() | ||||
|             { | ||||
|                 FileProvider = new PhysicalFileProvider(Configuration["CacheDirectory"]), | ||||
|                 RequestPath = "/cache", | ||||
|                 ServeUnknownFileTypes = true | ||||
|             }); | ||||
|  | ||||
|             app.UseEndpoints(endpoints => | ||||
|             { | ||||
|   | ||||
| @@ -23,6 +23,8 @@ | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   "DiscordServerUrl": "", | ||||
|   "DiscordBotToken": "", | ||||
|   "UnusedFileRetentionPeriodInDays": 7, | ||||
|   "PurgeUnusedAccounts": true, | ||||
|   "PurgeUnusedAccountsPeriodInDays": 14, | ||||
| @@ -35,7 +37,7 @@ | ||||
|         "Certificate": { | ||||
|           "Subject": "darkarchon.internet-box.ch", | ||||
|           "Store": "My", | ||||
|           "Location": "LocalMachine", | ||||
|           "Location": "LocalMachine" | ||||
|           //"AllowInvalid": false | ||||
|           //          "Path": "", //use path, keypath and password to provide a valid certificate if not using windows key store | ||||
|           //          "KeyPath": "" | ||||
| @@ -43,5 +45,23 @@ | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "IpRateLimiting": { | ||||
|     "EnableEndpointRateLimiting": false, | ||||
|     "StackBlockedRequests": false, | ||||
|     "RealIpHeader": "X-Real-IP", | ||||
|     "ClientIdHeader": "X-ClientId", | ||||
|     "HttpStatusCode": 429, | ||||
|     "IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ], | ||||
|     "GeneralRules": [ | ||||
|       { | ||||
|         "Endpoint": "*", | ||||
|         "Period": "1s", | ||||
|         "Limit": 2 | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "IPRateLimitPolicies": { | ||||
|           "IpRules": [] | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 rootdarkarchon
					rootdarkarchon