diff --git a/MareSynchronosServer/MareSynchronosServer/Authentication/SecretKeyAuthenticationHandler.cs b/MareSynchronosServer/MareSynchronosServer/Authentication/SecretKeyAuthenticationHandler.cs index b69baf4..2e5c305 100644 --- a/MareSynchronosServer/MareSynchronosServer/Authentication/SecretKeyAuthenticationHandler.cs +++ b/MareSynchronosServer/MareSynchronosServer/Authentication/SecretKeyAuthenticationHandler.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Concurrent; 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 Microsoft.AspNetCore.Authentication; @@ -17,6 +20,29 @@ namespace MareSynchronosServer.Authentication { private readonly MareDbContext _mareDbContext; public const string AuthScheme = "SecretKeyAuth"; + private const string unauthorized = "Unauthorized"; + public static ConcurrentDictionary Authentications = new(); + private static SemaphoreSlim dbLockSemaphore = new SemaphoreSlim(20); + + public static void ClearUnauthorizedUsers() + { + foreach (var item in Authentications.ToArray()) + { + if (item.Value == unauthorized) + { + Authentications[item.Key] = string.Empty; + } + } + } + + public static void RemoveAuthentication(string uid) + { + var auth = Authentications.Where(u => u.Value == uid); + if (auth.Any()) + { + Authentications.Remove(auth.First().Key, out _); + } + } protected override async Task HandleAuthenticateAsync() { @@ -30,12 +56,38 @@ namespace MareSynchronosServer.Authentication using var sha256 = SHA256.Create(); var hashedHeader = BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(authHeader))).Replace("-", ""); - var uid = (await _mareDbContext.Auth.Include("User").AsNoTracking() - .FirstOrDefaultAsync(m => m.HashedKey == hashedHeader))?.UserUID; - if (uid == null) + if (Authentications.TryGetValue(hashedHeader, out string uid)) { - return AuthenticateResult.Fail("Failed Authorization"); + if (uid == unauthorized) + return AuthenticateResult.Fail("Failed Authorization"); + else + Logger.LogDebug("Found cached entry for " + uid); + } + + if (string.IsNullOrEmpty(uid)) + { + try + { + dbLockSemaphore.Wait(); + uid = (await _mareDbContext.Auth.Include("User").AsNoTracking() + .FirstOrDefaultAsync(m => m.HashedKey == hashedHeader))?.UserUID; + } + catch { } + finally + { + dbLockSemaphore.Release(); + } + + if (uid == null) + { + Authentications[hashedHeader] = unauthorized; + return AuthenticateResult.Fail("Failed Authorization"); + } + else + { + Authentications[hashedHeader] = uid; + } } var claims = new List { diff --git a/MareSynchronosServer/MareSynchronosServer/FileCleanupService.cs b/MareSynchronosServer/MareSynchronosServer/CleanupService.cs similarity index 88% rename from MareSynchronosServer/MareSynchronosServer/FileCleanupService.cs rename to MareSynchronosServer/MareSynchronosServer/CleanupService.cs index f54c963..6b71283 100644 --- a/MareSynchronosServer/MareSynchronosServer/FileCleanupService.cs +++ b/MareSynchronosServer/MareSynchronosServer/CleanupService.cs @@ -4,6 +4,7 @@ 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; @@ -15,14 +16,14 @@ using Microsoft.Extensions.Logging; namespace MareSynchronosServer { - public class FileCleanupService : IHostedService, IDisposable + public class CleanupService : IHostedService, IDisposable { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServiceProvider _services; private readonly IConfiguration _configuration; private Timer _timer; - public FileCleanupService(ILogger logger, IServiceProvider services, IConfiguration configuration) + public CleanupService(ILogger logger, IServiceProvider services, IConfiguration configuration) { _logger = logger; _services = services; @@ -31,14 +32,14 @@ namespace MareSynchronosServer public Task StartAsync(CancellationToken cancellationToken) { - _logger.LogInformation("File Cleanup Service started"); + _logger.LogInformation("Cleanup Service started"); - _timer = new Timer(CleanUpFiles, null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); + _timer = new Timer(CleanUp, null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); return Task.CompletedTask; } - private void CleanUpFiles(object state) + private void CleanUp(object state) { if (!int.TryParse(_configuration["UnusedFileRetentionPeriodInDays"], out var filesOlderThanDays)) { @@ -73,6 +74,7 @@ namespace MareSynchronosServer } } + _logger.LogInformation($"Cleaning up expired lodestone authentications"); var lodestoneAuths = dbContext.LodeStoneAuth.Include(u => u.User).Where(a => a.StartedAt != null).ToList(); List expiredAuths = new List(); foreach (var auth in lodestoneAuths) @@ -118,6 +120,9 @@ namespace MareSynchronosServer } } + _logger.LogInformation("Cleaning up unauthorized users"); + SecretKeyAuthenticationHandler.ClearUnauthorizedUsers(); + _logger.LogInformation($"Cleanup complete"); dbContext.SaveChanges(); @@ -136,6 +141,8 @@ namespace MareSynchronosServer dbContext.Remove(lodestone); } + SecretKeyAuthenticationHandler.RemoveAuthentication(user.UID); + var auth = dbContext.Auth.Single(a => a.UserUID == user.UID); var userFiles = dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == user.UID).ToList(); diff --git a/MareSynchronosServer/MareSynchronosServer/Data/MareDbContext.cs b/MareSynchronosServer/MareSynchronosServer/Data/MareDbContext.cs index a6975d2..571436a 100644 --- a/MareSynchronosServer/MareSynchronosServer/Data/MareDbContext.cs +++ b/MareSynchronosServer/MareSynchronosServer/Data/MareDbContext.cs @@ -16,6 +16,7 @@ namespace MareSynchronosServer.Data 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) @@ -32,6 +33,7 @@ namespace MareSynchronosServer.Data 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 index 0af9160..f0bf51e 100644 --- a/MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs +++ b/MareSynchronosServer/MareSynchronosServer/Discord/DiscordBot.cs @@ -115,7 +115,7 @@ namespace MareSynchronosServer.Discord { if (discordAuthedUser.User != null) { - FileCleanupService.PurgeUser(discordAuthedUser.User, db, configuration); + CleanupService.PurgeUser(discordAuthedUser.User, db, configuration); } else { @@ -250,7 +250,13 @@ namespace MareSynchronosServer.Discord var db = scope.ServiceProvider.GetService(); - if (db.LodeStoneAuth.Any(a => a.DiscordId == arg.User.Id)) + // 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"); @@ -302,7 +308,7 @@ namespace MareSynchronosServer.Discord return auth; } - private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl) + 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); @@ -311,7 +317,8 @@ namespace MareSynchronosServer.Discord lodestoneUrl = matches.Groups[0].ToString(); var stringId = lodestoneUrl.Split('/', StringSplitOptions.RemoveEmptyEntries).Last(); - if (!int.TryParse(stringId, out int lodestoneId)) { + if (!int.TryParse(stringId, out int lodestoneId)) + { return null; } diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.Designer.cs b/MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.Designer.cs new file mode 100644 index 0000000..f7d9af7 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.Designer.cs @@ -0,0 +1,295 @@ +// +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("20220806103053_AddBannedRegistrations")] + partial class AddBannedRegistrations + { + 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("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("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("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasColumnType("text") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("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("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("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("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasColumnType("text") + .HasColumnName("forbidden_by"); + + b.Property("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("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasColumnType("text") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasColumnType("text") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("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("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("CharacterIdentification") + .HasColumnType("text") + .HasColumnName("character_identification"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("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 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.cs b/MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.cs new file mode 100644 index 0000000..954eb2d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Migrations/20220806103053_AddBannedRegistrations.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class AddBannedRegistrations : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "banned_registrations", + columns: table => new + { + discord_id_or_lodestone_auth = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_banned_registrations", x => x.discord_id_or_lodestone_auth); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "banned_registrations"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Migrations/MareDbContextModelSnapshot.cs b/MareSynchronosServer/MareSynchronosServer/Migrations/MareDbContextModelSnapshot.cs index 610fa11..ea90d28 100644 --- a/MareSynchronosServer/MareSynchronosServer/Migrations/MareDbContextModelSnapshot.cs +++ b/MareSynchronosServer/MareSynchronosServer/Migrations/MareDbContextModelSnapshot.cs @@ -64,6 +64,18 @@ namespace MareSynchronosServer.Migrations b.ToTable("banned_users", (string)null); }); + modelBuilder.Entity("MareSynchronosServer.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasColumnType("text") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + modelBuilder.Entity("MareSynchronosServer.Models.ClientPair", b => { b.Property("UserUID") diff --git a/MareSynchronosServer/MareSynchronosServer/Models/BannedRegistrations.cs b/MareSynchronosServer/MareSynchronosServer/Models/BannedRegistrations.cs new file mode 100644 index 0000000..ca107a7 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Models/BannedRegistrations.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosServer.Models +{ + public class BannedRegistrations + { + [Key] + public string DiscordIdOrLodestoneAuth { get; set; } + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Startup.cs b/MareSynchronosServer/MareSynchronosServer/Startup.cs index 5166886..a6656af 100644 --- a/MareSynchronosServer/MareSynchronosServer/Startup.cs +++ b/MareSynchronosServer/MareSynchronosServer/Startup.cs @@ -57,7 +57,7 @@ namespace MareSynchronosServer }).UseSnakeCaseNamingConvention(); }); - services.AddHostedService(); + services.AddHostedService(); services.AddHostedService(provider => provider.GetService()); services.AddHostedService();