From 8b363a06d0b9970d948df295ecb7f8990cd66c13 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 21 Mar 2025 11:23:52 +0100 Subject: [PATCH 1/2] Intercept cloudflare turnstile requests --- API.IntegrationTests/AccountTests.cs | 2 +- .../CloudflareTurnstileHttpMessageHandler.cs | 70 +++++++++++++++++++ .../IntegrationTestWebAppFactory.cs | 7 ++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 API.IntegrationTests/HttpMessageHandlers/CloudflareTurnstileHttpMessageHandler.cs diff --git a/API.IntegrationTests/AccountTests.cs b/API.IntegrationTests/AccountTests.cs index 0d0a9b08..99e8a47b 100644 --- a/API.IntegrationTests/AccountTests.cs +++ b/API.IntegrationTests/AccountTests.cs @@ -16,7 +16,7 @@ public async Task CreateAccount_ShouldAdd_NewUserToDatabase() username = "Bob", password = "SecurePassword123#", email = "bob@example.com", - turnstileresponse = "lmao" + turnstileresponse = "valid-token" }); diff --git a/API.IntegrationTests/HttpMessageHandlers/CloudflareTurnstileHttpMessageHandler.cs b/API.IntegrationTests/HttpMessageHandlers/CloudflareTurnstileHttpMessageHandler.cs new file mode 100644 index 00000000..cd23d728 --- /dev/null +++ b/API.IntegrationTests/HttpMessageHandlers/CloudflareTurnstileHttpMessageHandler.cs @@ -0,0 +1,70 @@ +using System.Net; +using System.Text.Json; +using System.Web; + +namespace OpenShock.API.IntegrationTests.HttpMessageHandlers; + +sealed class CloudflareTurnstileHttpMessageHandler : DelegatingHandler +{ + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.RequestUri is null || !request.RequestUri.AbsoluteUri.EndsWith("siteverify")) + { + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + + var formData = request.Content != null ? await request.Content.ReadAsStringAsync(cancellationToken) : string.Empty; + var parsedForm = HttpUtility.ParseQueryString(formData); + var responseToken = parsedForm["response"]; + + var responseDto = responseToken switch + { + "valid-token" => new CloudflareTurnstileVerifyResponseDto + { + Success = true, + ErrorCodes = [], + ChallengeTs = DateTime.UtcNow, + Hostname = "validhost", + Action = "validaction", + Cdata = "" + }, + "invalid-token" => new CloudflareTurnstileVerifyResponseDto + { + Success = false, + ErrorCodes = ["invalid-input-response"], + ChallengeTs = DateTime.UtcNow, + Hostname = "invalidhost", + Action = "invalidaction", + Cdata = "" + }, + _ => new CloudflareTurnstileVerifyResponseDto + { + Success = false, + ErrorCodes = ["bad-request"], + ChallengeTs = DateTime.UtcNow, + Hostname = "unknownhost", + Action = "unknownaction", + Cdata = "" + } + }; + + var responseJson = JsonSerializer.Serialize(responseDto); + + var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseJson, System.Text.Encoding.UTF8, "application/json") + }; + + return responseMessage; + } + + private class CloudflareTurnstileVerifyResponseDto + { + public bool Success { get; set; } + public string[] ErrorCodes { get; set; } + public DateTime ChallengeTs { get; set; } + public string Hostname { get; set; } + public string Action { get; set; } + public string Cdata { get; set; } + } +} \ No newline at end of file diff --git a/API.IntegrationTests/IntegrationTestWebAppFactory.cs b/API.IntegrationTests/IntegrationTestWebAppFactory.cs index e0d4bd5c..8e107eca 100644 --- a/API.IntegrationTests/IntegrationTestWebAppFactory.cs +++ b/API.IntegrationTests/IntegrationTestWebAppFactory.cs @@ -1,6 +1,10 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenShock.API.IntegrationTests.HttpMessageHandlers; +using OpenShock.Common.Services.Turnstile; using Testcontainers.PostgreSql; using Testcontainers.Redis; using TUnit.Core.Interfaces; @@ -78,6 +82,9 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.ConfigureTestServices(services => { + services.RemoveAll(); + services.AddHttpClient() + .AddHttpMessageHandler(); // We can replace services here }); } From 33b6d523d26ca61412555242b25f5b6f7c1329f1 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 21 Mar 2025 12:26:41 +0100 Subject: [PATCH 2/2] Less invasive http request intercepting, fix DI --- ...er.cs => InterceptedHttpMessageHandler.cs} | 24 +++++++++++++------ .../InterceptedHttpMessageHandlerBuilder.cs | 16 +++++++++++++ .../IntegrationTestWebAppFactory.cs | 6 ++--- 3 files changed, 35 insertions(+), 11 deletions(-) rename API.IntegrationTests/HttpMessageHandlers/{CloudflareTurnstileHttpMessageHandler.cs => InterceptedHttpMessageHandler.cs} (73%) create mode 100644 API.IntegrationTests/HttpMessageHandlers/InterceptedHttpMessageHandlerBuilder.cs diff --git a/API.IntegrationTests/HttpMessageHandlers/CloudflareTurnstileHttpMessageHandler.cs b/API.IntegrationTests/HttpMessageHandlers/InterceptedHttpMessageHandler.cs similarity index 73% rename from API.IntegrationTests/HttpMessageHandlers/CloudflareTurnstileHttpMessageHandler.cs rename to API.IntegrationTests/HttpMessageHandlers/InterceptedHttpMessageHandler.cs index cd23d728..7302613b 100644 --- a/API.IntegrationTests/HttpMessageHandlers/CloudflareTurnstileHttpMessageHandler.cs +++ b/API.IntegrationTests/HttpMessageHandlers/InterceptedHttpMessageHandler.cs @@ -4,15 +4,10 @@ namespace OpenShock.API.IntegrationTests.HttpMessageHandlers; -sealed class CloudflareTurnstileHttpMessageHandler : DelegatingHandler +sealed class InterceptedHttpMessageHandler : DelegatingHandler { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + private async Task HandleCloudflareTurnstileRequest(HttpRequestMessage request, CancellationToken cancellationToken) { - if (request.RequestUri is null || !request.RequestUri.AbsoluteUri.EndsWith("siteverify")) - { - return new HttpResponseMessage(HttpStatusCode.NotFound); - } - var formData = request.Content != null ? await request.Content.ReadAsStringAsync(cancellationToken) : string.Empty; var parsedForm = HttpUtility.ParseQueryString(formData); var responseToken = parsedForm["response"]; @@ -58,6 +53,21 @@ protected override async Task SendAsync(HttpRequestMessage return responseMessage; } + private async Task HandleMailJetApiHost(HttpRequestMessage request, CancellationToken cancellationToken) + { + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return request.RequestUri switch + { + { Host: "challenges.cloudflare.com", AbsolutePath: "/turnstile/v0/siteverify" } => await HandleCloudflareTurnstileRequest(request, cancellationToken), + { Host: "api.mailjet.com" } => await HandleMailJetApiHost(request, cancellationToken), + _ => new HttpResponseMessage(HttpStatusCode.NotFound) + }; + } + private class CloudflareTurnstileVerifyResponseDto { public bool Success { get; set; } diff --git a/API.IntegrationTests/HttpMessageHandlers/InterceptedHttpMessageHandlerBuilder.cs b/API.IntegrationTests/HttpMessageHandlers/InterceptedHttpMessageHandlerBuilder.cs new file mode 100644 index 00000000..f4444f8e --- /dev/null +++ b/API.IntegrationTests/HttpMessageHandlers/InterceptedHttpMessageHandlerBuilder.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Http; + +namespace OpenShock.API.IntegrationTests.HttpMessageHandlers; + +sealed class InterceptedHttpMessageHandlerBuilder : HttpMessageHandlerBuilder +{ + public override string? Name { get; set; } + public override HttpMessageHandler PrimaryHandler { get; set; } + public override IList AdditionalHandlers => []; + + + public override HttpMessageHandler Build() + { + return new InterceptedHttpMessageHandler(); + } +} diff --git a/API.IntegrationTests/IntegrationTestWebAppFactory.cs b/API.IntegrationTests/IntegrationTestWebAppFactory.cs index 8e107eca..80d3b2a9 100644 --- a/API.IntegrationTests/IntegrationTestWebAppFactory.cs +++ b/API.IntegrationTests/IntegrationTestWebAppFactory.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http; using OpenShock.API.IntegrationTests.HttpMessageHandlers; using OpenShock.Common.Services.Turnstile; using Testcontainers.PostgreSql; @@ -82,10 +83,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.ConfigureTestServices(services => { - services.RemoveAll(); - services.AddHttpClient() - .AddHttpMessageHandler(); - // We can replace services here + services.AddTransient(); }); }