diff --git a/.idea/.idea.ProjectLighthouse/.idea/jsLibraryMappings.xml b/.idea/.idea.ProjectLighthouse/.idea/jsLibraryMappings.xml index e0f60e14..6f9a9516 100644 --- a/.idea/.idea.ProjectLighthouse/.idea/jsLibraryMappings.xml +++ b/.idea/.idea.ProjectLighthouse/.idea/jsLibraryMappings.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/CaptchaHelper.cs b/ProjectLighthouse/Helpers/CaptchaHelper.cs new file mode 100644 index 00000000..3008b084 --- /dev/null +++ b/ProjectLighthouse/Helpers/CaptchaHelper.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Types.Settings; +using Newtonsoft.Json.Linq; + +namespace LBPUnion.ProjectLighthouse.Helpers; + +public static class CaptchaHelper +{ + private static readonly HttpClient client = new() + { + BaseAddress = new Uri("https://hcaptcha.com"), + }; + + public static async Task Verify(string token) + { + if (!ServerSettings.Instance.HCaptchaEnabled) return true; + + List> payload = new() + { + new("secret", ServerSettings.Instance.HCaptchaSecret), + new("response", token), + }; + + HttpResponseMessage response = await client.PostAsync("/siteverify", new FormUrlEncodedContent(payload)); + + response.EnsureSuccessStatusCode(); + + string responseBody = await response.Content.ReadAsStringAsync(); + + // We only really care about the success result, nothing else that hcaptcha sends us, so lets only parse that. + bool success = bool.Parse(JObject.Parse(responseBody)["success"]?.ToString() ?? "false"); + return success; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/Extensions/RequestExtensions.cs b/ProjectLighthouse/Helpers/Extensions/RequestExtensions.cs index 05208124..b8738c4d 100644 --- a/ProjectLighthouse/Helpers/Extensions/RequestExtensions.cs +++ b/ProjectLighthouse/Helpers/Extensions/RequestExtensions.cs @@ -1,5 +1,9 @@ +#nullable enable using System.Text.RegularExpressions; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Types.Settings; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace LBPUnion.ProjectLighthouse.Helpers.Extensions; @@ -11,4 +15,17 @@ public static class RequestExtensions ("Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled); public static bool IsMobile(this HttpRequest request) => mobileCheck.IsMatch(request.Headers[HeaderNames.UserAgent].ToString()); + + public static async Task CheckCaptchaValidity(this HttpRequest request) + { + if (ServerSettings.Instance.HCaptchaEnabled) + { + bool gotCaptcha = request.Form.TryGetValue("h-captcha-response", out StringValues values); + if (!gotCaptcha) return false; + + if (!await CaptchaHelper.Verify(values[0])) return false; + } + + return true; + } } \ No newline at end of file diff --git a/ProjectLighthouse/Pages/LoginForm.cshtml b/ProjectLighthouse/Pages/LoginForm.cshtml index 2e5494a4..f07daa9f 100644 --- a/ProjectLighthouse/Pages/LoginForm.cshtml +++ b/ProjectLighthouse/Pages/LoginForm.cshtml @@ -50,6 +50,11 @@ + @if (ServerSettings.Instance.HCaptchaEnabled) + { + @await Html.PartialAsync("Partials/CaptchaPartial") + } + @if (ServerSettings.Instance.RegistrationEnabled) { diff --git a/ProjectLighthouse/Pages/LoginForm.cshtml.cs b/ProjectLighthouse/Pages/LoginForm.cshtml.cs index 5e19c5a4..7ceb63d3 100644 --- a/ProjectLighthouse/Pages/LoginForm.cshtml.cs +++ b/ProjectLighthouse/Pages/LoginForm.cshtml.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Kettu; using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Helpers.Extensions; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Pages.Layouts; using LBPUnion.ProjectLighthouse.Types; @@ -18,8 +19,6 @@ public class LoginForm : BaseLayout public string Error { get; private set; } - public bool WasLoginRequest { get; private set; } - [UsedImplicitly] public async Task OnPost(string username, string password) { @@ -35,6 +34,12 @@ public class LoginForm : BaseLayout return this.Page(); } + if (!await Request.CheckCaptchaValidity()) + { + this.Error = "You must complete the captcha correctly."; + return this.Page(); + } + User? user = await this.Database.Users.FirstOrDefaultAsync(u => u.Username == username); if (user == null) { @@ -68,6 +73,8 @@ public class LoginForm : BaseLayout this.Response.Cookies.Append("LighthouseToken", webToken.UserToken); + Logger.Log($"User {user.Username} (id: {user.UserId}) successfully logged in on web", LoggerLevelLogin.Instance); + if (user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired"); return this.RedirectToPage(nameof(LandingPage)); diff --git a/ProjectLighthouse/Pages/Partials/CaptchaPartial.cshtml b/ProjectLighthouse/Pages/Partials/CaptchaPartial.cshtml new file mode 100644 index 00000000..e1891722 --- /dev/null +++ b/ProjectLighthouse/Pages/Partials/CaptchaPartial.cshtml @@ -0,0 +1,6 @@ +@using LBPUnion.ProjectLighthouse.Types.Settings +@if (ServerSettings.Instance.HCaptchaEnabled) +{ +
+ +} \ No newline at end of file diff --git a/ProjectLighthouse/Pages/RegisterForm.cshtml b/ProjectLighthouse/Pages/RegisterForm.cshtml index 67b3a05a..54775d85 100644 --- a/ProjectLighthouse/Pages/RegisterForm.cshtml +++ b/ProjectLighthouse/Pages/RegisterForm.cshtml @@ -1,4 +1,5 @@ @page "/register" +@using LBPUnion.ProjectLighthouse.Types.Settings @model LBPUnion.ProjectLighthouse.Pages.RegisterForm @{ @@ -60,5 +61,10 @@ + @if (ServerSettings.Instance.HCaptchaEnabled) + { + @await Html.PartialAsync("Partials/CaptchaPartial") + } + \ No newline at end of file diff --git a/ProjectLighthouse/Pages/RegisterForm.cshtml.cs b/ProjectLighthouse/Pages/RegisterForm.cshtml.cs index 310884d5..a7945459 100644 --- a/ProjectLighthouse/Pages/RegisterForm.cshtml.cs +++ b/ProjectLighthouse/Pages/RegisterForm.cshtml.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using JetBrains.Annotations; using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Helpers.Extensions; using LBPUnion.ProjectLighthouse.Pages.Layouts; using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Settings; @@ -42,13 +43,18 @@ public class RegisterForm : BaseLayout return this.Page(); } - bool userExists = await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null; - if (userExists) + if (await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null) { this.Error = "The username you've chosen is already taken."; return this.Page(); } + if (!await Request.CheckCaptchaValidity()) + { + this.Error = "You must complete the captcha correctly."; + return this.Page(); + } + User user = await this.Database.CreateUser(username, HashHelper.BCryptHash(password)); WebToken webToken = new() diff --git a/ProjectLighthouse/Types/Settings/ServerSettings.cs b/ProjectLighthouse/Types/Settings/ServerSettings.cs index 8948ca69..f6142538 100644 --- a/ProjectLighthouse/Types/Settings/ServerSettings.cs +++ b/ProjectLighthouse/Types/Settings/ServerSettings.cs @@ -5,7 +5,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using JetBrains.Annotations; using Kettu; -using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; namespace LBPUnion.ProjectLighthouse.Types.Settings; @@ -13,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings; [Serializable] public class ServerSettings { - public const int CurrentConfigVersion = 19; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE! + public const int CurrentConfigVersion = 20; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE! private static FileSystemWatcher fileWatcher; static ServerSettings() { @@ -156,6 +155,12 @@ public class ServerSettings public string MissingIconHash { get; set; } = ""; + public bool HCaptchaEnabled { get; set; } + + public string HCaptchaSiteKey { get; set; } = ""; + + public string HCaptchaSecret { get; set; } = ""; + #region Meta [NotNull]