From 80880a18a89734de9878d011900cd31677f36622 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 11 Mar 2022 15:59:08 +0000 Subject: [PATCH] Add hCaptcha to login page --- VoidCat/Controllers/AuthController.cs | 27 ++++++++--- VoidCat/Controllers/InfoController.cs | 44 ++++++++++++++++++ VoidCat/Controllers/StatsController.cs | 24 ---------- VoidCat/Model/VoidSettings.cs | 10 +++++ VoidCat/Program.cs | 15 ++++++- .../Services/Abstractions/ICaptchaVerifier.cs | 6 +++ VoidCat/Services/Captcha/CaptchaStartup.cs | 19 ++++++++ VoidCat/Services/Captcha/NoOpVerifier.cs | 11 +++++ VoidCat/Services/Captcha/hCaptchaVerifier.cs | 45 +++++++++++++++++++ VoidCat/spa/package.json | 1 + VoidCat/spa/src/Api.js | 6 +-- VoidCat/spa/src/GlobalStats.js | 14 ++---- VoidCat/spa/src/Header.js | 8 ++++ VoidCat/spa/src/Login.js | 8 +++- VoidCat/spa/src/SiteInfoStore.js | 16 +++++++ VoidCat/spa/src/Store.js | 4 +- VoidCat/spa/yarn.lock | 5 +++ 17 files changed, 214 insertions(+), 49 deletions(-) create mode 100644 VoidCat/Controllers/InfoController.cs create mode 100644 VoidCat/Services/Abstractions/ICaptchaVerifier.cs create mode 100644 VoidCat/Services/Captcha/CaptchaStartup.cs create mode 100644 VoidCat/Services/Captcha/NoOpVerifier.cs create mode 100644 VoidCat/Services/Captcha/hCaptchaVerifier.cs create mode 100644 VoidCat/spa/src/SiteInfoStore.js diff --git a/VoidCat/Controllers/AuthController.cs b/VoidCat/Controllers/AuthController.cs index 4b06a63..fe3b8d0 100644 --- a/VoidCat/Controllers/AuthController.cs +++ b/VoidCat/Controllers/AuthController.cs @@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; -using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using VoidCat.Model; @@ -15,11 +14,13 @@ public class AuthController : Controller { private readonly IUserManager _manager; private readonly VoidSettings _settings; + private readonly ICaptchaVerifier _captchaVerifier; - public AuthController(IUserManager userStore, VoidSettings settings) + public AuthController(IUserManager userStore, VoidSettings settings, ICaptchaVerifier captchaVerifier) { _manager = userStore; _settings = settings; + _captchaVerifier = captchaVerifier; } /// @@ -39,6 +40,12 @@ public class AuthController : Controller return new(null, error); } + // check captcha + if (!await _captchaVerifier.Verify(req.Captcha)) + { + return new(null, "Captcha verification failed"); + } + var user = await _manager.Login(req.Username, req.Password); var token = CreateToken(user); var tokenWriter = new JwtSecurityTokenHandler(); @@ -67,6 +74,12 @@ public class AuthController : Controller return new(null, error); } + // check captcha + if (!await _captchaVerifier.Verify(req.Captcha)) + { + return new(null, "Captcha verification failed"); + } + var newUser = await _manager.Register(req.Username, req.Password); var token = CreateToken(newUser); var tokenWriter = new JwtSecurityTokenHandler(); @@ -96,7 +109,7 @@ public class AuthController : Controller } - public class LoginRequest + public sealed class LoginRequest { public LoginRequest(string username, string password) { @@ -106,12 +119,14 @@ public class AuthController : Controller [Required] [EmailAddress] - public string Username { get; init; } + public string Username { get; } [Required] [MinLength(6)] - public string Password { get; init; } + public string Password { get; } + + public string? Captcha { get; init; } } - public record LoginResponse(string? Jwt, string? Error = null, VoidUser? Profile = null); + public sealed record LoginResponse(string? Jwt, string? Error = null, VoidUser? Profile = null); } diff --git a/VoidCat/Controllers/InfoController.cs b/VoidCat/Controllers/InfoController.cs new file mode 100644 index 0000000..5820017 --- /dev/null +++ b/VoidCat/Controllers/InfoController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Controllers; + +[Route("info")] +public class InfoController : Controller +{ + private readonly IStatsReporter _statsReporter; + private readonly IFileStore _fileStore; + private readonly VoidSettings _settings; + + public InfoController(IStatsReporter statsReporter, IFileStore fileStore, VoidSettings settings) + { + _statsReporter = statsReporter; + _fileStore = fileStore; + _settings = settings; + } + + /// + /// Return system info + /// + /// + [HttpGet] + [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)] + public async Task GetGlobalStats() + { + var bw = await _statsReporter.GetBandwidth(); + var bytes = 0UL; + var count = 0; + var files = await _fileStore.ListFiles(new(0, Int32.MaxValue)); + await foreach (var vf in files.Results) + { + bytes += vf.Metadata?.Size ?? 0; + count++; + } + + return new(bw, bytes, count, BuildInfo.GetBuildInfo(), _settings.CaptchaSettings?.SiteKey); + } + + public sealed record GlobalInfo(Bandwidth Bandwidth, ulong TotalBytes, int Count, BuildInfo BuildInfo, + string? CaptchaSiteKey); +} \ No newline at end of file diff --git a/VoidCat/Controllers/StatsController.cs b/VoidCat/Controllers/StatsController.cs index 7debfe0..359cf4f 100644 --- a/VoidCat/Controllers/StatsController.cs +++ b/VoidCat/Controllers/StatsController.cs @@ -16,28 +16,6 @@ namespace VoidCat.Controllers _fileStore = fileStore; } - - /// - /// Return system info - /// - /// - [HttpGet] - [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)] - public async Task GetGlobalStats() - { - var bw = await _statsReporter.GetBandwidth(); - var bytes = 0UL; - var count = 0; - var files = await _fileStore.ListFiles(new(0, Int32.MaxValue)); - await foreach (var vf in files.Results) - { - bytes += vf.Metadata?.Size ?? 0; - count++; - } - - return new(bw, bytes, count, BuildInfo.GetBuildInfo()); - } - /// /// Get stats for a specific file /// @@ -52,7 +30,5 @@ namespace VoidCat.Controllers } } - public sealed record GlobalStats(Bandwidth Bandwidth, ulong TotalBytes, int Count, BuildInfo BuildInfo); - public sealed record FileStats(Bandwidth Bandwidth); } \ No newline at end of file diff --git a/VoidCat/Model/VoidSettings.cs b/VoidCat/Model/VoidSettings.cs index 09af0a1..ffa47f8 100644 --- a/VoidCat/Model/VoidSettings.cs +++ b/VoidCat/Model/VoidSettings.cs @@ -25,6 +25,10 @@ namespace VoidCat.Model public CloudStorageSettings? CloudStorage { get; init; } public VirusScannerSettings? VirusScanner { get; init; } + + public IEnumerable? RequestHeadersLog { get; init; } + + public CaptchaSettings? CaptchaSettings { get; init; } } public sealed class TorSettings @@ -78,4 +82,10 @@ namespace VoidCat.Model { public string? ApiKey { get; init; } } + + public sealed class CaptchaSettings + { + public string? SiteKey { get; init; } + public string? Secret { get; init; } + } } \ No newline at end of file diff --git a/VoidCat/Program.cs b/VoidCat/Program.cs index 70ce4fe..15cb3e3 100644 --- a/VoidCat/Program.cs +++ b/VoidCat/Program.cs @@ -11,6 +11,7 @@ using VoidCat.Model; using VoidCat.Services; using VoidCat.Services.Abstractions; using VoidCat.Services.Background; +using VoidCat.Services.Captcha; using VoidCat.Services.Files; using VoidCat.Services.InMemory; using VoidCat.Services.Migrations; @@ -52,10 +53,17 @@ if (useRedis) services.AddHttpLogging((o) => { - o.LoggingFields = HttpLoggingFields.RequestHeaders | HttpLoggingFields.ResponseHeaders | HttpLoggingFields.Response; + o.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.ResponsePropertiesAndHeaders; o.RequestBodyLogLimit = 4096; o.ResponseBodyLogLimit = 4096; + + o.MediaTypeOptions.Clear(); o.MediaTypeOptions.AddText("application/json"); + + foreach (var h in voidSettings.RequestHeadersLog ?? Enumerable.Empty()) + { + o.RequestHeaders.Add(h); + } }); services.AddHttpClient(); services.AddSwaggerGen(c => @@ -143,6 +151,9 @@ services.AddHostedService(); // virus scanner services.AddVirusScanner(voidSettings); +// captcha +services.AddCaptcha(voidSettings); + if (useRedis) { services.AddTransient(); @@ -172,7 +183,7 @@ foreach (var migration in migrations) app.UseStaticFiles(); #endif -app.UseHttpLogging(); +app.UseHttpLogging(); app.UseRouting(); app.UseCors(); app.UseSwagger(); diff --git a/VoidCat/Services/Abstractions/ICaptchaVerifier.cs b/VoidCat/Services/Abstractions/ICaptchaVerifier.cs new file mode 100644 index 0000000..645f0f2 --- /dev/null +++ b/VoidCat/Services/Abstractions/ICaptchaVerifier.cs @@ -0,0 +1,6 @@ +namespace VoidCat.Services.Abstractions; + +public interface ICaptchaVerifier +{ + ValueTask Verify(string? token); +} \ No newline at end of file diff --git a/VoidCat/Services/Captcha/CaptchaStartup.cs b/VoidCat/Services/Captcha/CaptchaStartup.cs new file mode 100644 index 0000000..c38f765 --- /dev/null +++ b/VoidCat/Services/Captcha/CaptchaStartup.cs @@ -0,0 +1,19 @@ +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.Captcha; + +public static class CaptchaStartup +{ + public static void AddCaptcha(this IServiceCollection services, VoidSettings settings) + { + if (settings.CaptchaSettings != default) + { + services.AddTransient(); + } + else + { + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/VoidCat/Services/Captcha/NoOpVerifier.cs b/VoidCat/Services/Captcha/NoOpVerifier.cs new file mode 100644 index 0000000..52ee9c0 --- /dev/null +++ b/VoidCat/Services/Captcha/NoOpVerifier.cs @@ -0,0 +1,11 @@ +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.Captcha; + +public class NoOpVerifier : ICaptchaVerifier +{ + public ValueTask Verify(string? token) + { + return ValueTask.FromResult(token == null); + } +} \ No newline at end of file diff --git a/VoidCat/Services/Captcha/hCaptchaVerifier.cs b/VoidCat/Services/Captcha/hCaptchaVerifier.cs new file mode 100644 index 0000000..1a5a20d --- /dev/null +++ b/VoidCat/Services/Captcha/hCaptchaVerifier.cs @@ -0,0 +1,45 @@ +using System.Net; +using Newtonsoft.Json; +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.Captcha; + +public class hCaptchaVerifier : ICaptchaVerifier +{ + private readonly IHttpClientFactory _clientFactory; + private readonly VoidSettings _settings; + + public hCaptchaVerifier(IHttpClientFactory clientFactory, VoidSettings settings) + { + _clientFactory = clientFactory; + _settings = settings; + } + + public async ValueTask Verify(string? token) + { + if (string.IsNullOrEmpty(token)) return false; + + var req = new HttpRequestMessage(HttpMethod.Post, "https://hcaptcha.com/siteverify"); + req.Content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("response", token), + new KeyValuePair("secret", _settings.CaptchaSettings!.Secret!) + }); + + using var cli = _clientFactory.CreateClient(); + var rsp = await cli.SendAsync(req); + if (rsp.StatusCode == HttpStatusCode.OK) + { + var body = JsonConvert.DeserializeObject(await rsp.Content.ReadAsStringAsync()); + return body?.Success == true; + } + + return false; + } + + internal sealed class hCaptchaResponse + { + public bool Success { get; init; } + } +} \ No newline at end of file diff --git a/VoidCat/spa/package.json b/VoidCat/spa/package.json index 5ddf725..9ebd4e5 100644 --- a/VoidCat/spa/package.json +++ b/VoidCat/spa/package.json @@ -4,6 +4,7 @@ "private": true, "proxy": "https://localhost:7195", "dependencies": { + "@hcaptcha/react-hcaptcha": "^1.1.1", "@reduxjs/toolkit": "^1.7.2", "feather-icons-react": "^0.5.0", "moment": "^2.29.1", diff --git a/VoidCat/spa/src/Api.js b/VoidCat/spa/src/Api.js index f6ce9bc..0e1c203 100644 --- a/VoidCat/spa/src/Api.js +++ b/VoidCat/spa/src/Api.js @@ -30,13 +30,13 @@ export function useApi() { userList: (pageReq) => getJson("POST", `/admin/user`, pageReq, auth) }, Api: { - stats: () => getJson("GET", "/stats"), + info: () => getJson("GET", "/info"), fileInfo: (id) => getJson("GET", `/upload/${id}`), setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, cfg, auth), createOrder: (id) => getJson("GET", `/upload/${id}/paywall`), getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`), - login: (username, password) => getJson("POST", `/auth/login`, {username, password}), - register: (username, password) => getJson("POST", `/auth/register`, {username, password}), + login: (username, password, captcha) => getJson("POST", `/auth/login`, {username, password, captcha}), + register: (username, password, captcha) => getJson("POST", `/auth/register`, {username, password, captcha}), getUser: (id) => getJson("GET", `/user/${id}`, undefined, auth), updateUser: (u) => getJson("POST", `/user/${u.id}`, u, auth), listUserFiles: (uid, pageReq) => getJson("POST", `/user/${uid}/files`, pageReq, auth), diff --git a/VoidCat/spa/src/GlobalStats.js b/VoidCat/spa/src/GlobalStats.js index 0d7ab1d..813299e 100644 --- a/VoidCat/spa/src/GlobalStats.js +++ b/VoidCat/spa/src/GlobalStats.js @@ -5,20 +5,12 @@ import {FormatBytes} from "./Util"; import "./GlobalStats.css"; import {useApi} from "./Api"; import moment from "moment"; +import {useSelector} from "react-redux"; export function GlobalStats(props) { const {Api} = useApi(); - let [stats, setStats] = useState(); - - async function loadStats() { - let req = await Api.stats(); - if (req.ok) { - setStats(await req.json()); - } - } - - useEffect(() => loadStats(), []); - + let stats = useSelector(state => state.info.stats); + return (
diff --git a/VoidCat/spa/src/Header.js b/VoidCat/spa/src/Header.js index af7f33f..f5c970e 100644 --- a/VoidCat/spa/src/Header.js +++ b/VoidCat/spa/src/Header.js @@ -5,6 +5,7 @@ import {InlineProfile} from "./InlineProfile"; import {useApi} from "./Api"; import {logout, setProfile} from "./LoginState"; import {useEffect} from "react"; +import {setStats} from "./SiteInfoStore"; export function Header() { const dispatch = useDispatch(); @@ -22,9 +23,16 @@ export function Header() { } } } + async function loadStats() { + let req = await Api.info(); + if (req.ok) { + dispatch(setStats(await req.json())); + } + } useEffect(() => { initProfile(); + loadStats(); }, []); return ( diff --git a/VoidCat/spa/src/Login.js b/VoidCat/spa/src/Login.js index 6898e13..37e4142 100644 --- a/VoidCat/spa/src/Login.js +++ b/VoidCat/spa/src/Login.js @@ -1,22 +1,25 @@ import {useState} from "react"; -import {useDispatch} from "react-redux"; +import {useDispatch, useSelector} from "react-redux"; import {setAuth} from "./LoginState"; import {useApi} from "./Api"; import "./Login.css"; import {btnDisable, btnEnable} from "./Util"; +import HCaptcha from "@hcaptcha/react-hcaptcha"; export function Login() { const {Api} = useApi(); const [username, setUsername] = useState(); const [password, setPassword] = useState(); const [error, setError] = useState(); + const [captchaResponse, setCaptchaResponse] = useState(); + const captchaKey = useSelector(state => state.info.stats.captchaSiteKey); const dispatch = useDispatch(); async function login(e, fnLogin) { if(!btnDisable(e.target)) return; setError(null); - let req = await fnLogin(username, password); + let req = await fnLogin(username, password, captchaResponse); if (req.ok) { let rsp = await req.json(); if (rsp.jwt) { @@ -38,6 +41,7 @@ export function Login() {
Password:
setPassword(e.target.value)}/>
+ {captchaKey ? : null}
login(e, Api.login)}>Login
login(e, Api.register)}>Register
{error ?
{error}
: null} diff --git a/VoidCat/spa/src/SiteInfoStore.js b/VoidCat/spa/src/SiteInfoStore.js new file mode 100644 index 0000000..d24badc --- /dev/null +++ b/VoidCat/spa/src/SiteInfoStore.js @@ -0,0 +1,16 @@ +import {createSlice} from "@reduxjs/toolkit"; + +export const SiteInfoState = createSlice({ + name: "SiteInfo", + initialState: { + stats: null + }, + reducers: { + setStats: (state, action) => { + state.stats = action.payload; + }, + } +}); + +export const {setStats} = SiteInfoState.actions; +export default SiteInfoState.reducer; \ No newline at end of file diff --git a/VoidCat/spa/src/Store.js b/VoidCat/spa/src/Store.js index 8c02117..ad55b26 100644 --- a/VoidCat/spa/src/Store.js +++ b/VoidCat/spa/src/Store.js @@ -1,8 +1,10 @@ import {configureStore} from "@reduxjs/toolkit"; import loginReducer from "./LoginState"; +import siteInfoReducer from "./SiteInfoStore"; export default configureStore({ reducer: { - login: loginReducer + login: loginReducer, + info: siteInfoReducer } }); \ No newline at end of file diff --git a/VoidCat/spa/yarn.lock b/VoidCat/spa/yarn.lock index 64bf4f6..ec8920f 100644 --- a/VoidCat/spa/yarn.lock +++ b/VoidCat/spa/yarn.lock @@ -1087,6 +1087,11 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@hcaptcha/react-hcaptcha@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.1.1.tgz#b7f9fef825fe52595e38b61f125dc8d87d2a5440" + integrity sha512-fydc0ob5NOEYiJPmohOmPqoBjLPhUokvMZBf1hOhrVcpMyZJbc1N/HPV4kysSayC34IQsaBRtEpswQFbHwe54w== + "@humanwhocodes/config-array@^0.9.2": version "0.9.2" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914"