add lodestone auth, add rate limiting middleware

This commit is contained in:
Stanley Dimant
2022-08-01 17:41:46 +02:00
parent 4d3226aa8b
commit e0f273a020
14 changed files with 1102 additions and 43 deletions

View File

@@ -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");
}
}
}

View 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();
}
}
}

View File

@@ -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();

View File

@@ -25,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);
@@ -130,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)

View File

@@ -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" />

View 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
}
}
}

View File

@@ -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");
}
}
}

View 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("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
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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();

View File

@@ -16,6 +16,8 @@ using Prometheus;
using WebSocketOptions = Microsoft.AspNetCore.Builder.WebSocketOptions;
using Microsoft.Extensions.FileProviders;
using Microsoft.AspNetCore.Authorization;
using MareSynchronosServer.Discord;
using AspNetCoreRateLimit;
namespace MareSynchronosServer
{
@@ -41,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);
@@ -55,6 +64,7 @@ namespace MareSynchronosServer
services.AddHostedService<FileCleanupService>();
services.AddHostedService(provider => provider.GetService<SystemInfoService>());
services.AddHostedService<DiscordBot>();
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddAuthentication(options =>
@@ -63,6 +73,8 @@ namespace MareSynchronosServer
})
.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();

View File

@@ -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": []
}
}