From 34451515dab860ed85daffe4b819c7abe83b78db Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Wed, 13 Mar 2024 16:18:05 +0100 Subject: [PATCH] hi --- .gitattributes | 63 +++++ .github/workflows/release.yml | 23 ++ .gitignore | 348 ++++++++++++++++++++++++ ITurnstilePolicy.cs | 10 + LICENSE.md | 24 ++ README.md | 1 + Turnstile.cs | 139 ++++++++++ TurnstileException.cs | 21 ++ TurnstileOptions.cs | 12 + TurnstileServiceCollectionExtensions.cs | 35 +++ TurnstileTag.csproj | 13 + TurnstileTag.sln | 25 ++ TurnstileTagHelper.cs | 42 +++ ValidateTurnstileAttribute.cs | 28 ++ ValidateTurnstileAuthorizationFilter.cs | 61 +++++ 15 files changed, 845 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 ITurnstilePolicy.cs create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 Turnstile.cs create mode 100644 TurnstileException.cs create mode 100644 TurnstileOptions.cs create mode 100644 TurnstileServiceCollectionExtensions.cs create mode 100644 TurnstileTag.csproj create mode 100644 TurnstileTag.sln create mode 100644 TurnstileTagHelper.cs create mode 100644 ValidateTurnstileAttribute.cs create mode 100644 ValidateTurnstileAuthorizationFilter.cs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..217b890 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Verify commit exists in origin/main + run: | + git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + git branch --remote --contains | grep origin/main + - name: Set VERSION variable from tag + run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV + - name: Build + run: dotnet build --configuration Release /p:Version=${VERSION} + - name: Pack + run: dotnet pack --configuration Release /p:Version=${VERSION} --no-build --output . + - name: Push + run: dotnet nuget push TurnstileTag.${VERSION}.nupkg -s https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_PUSH_API_KEY }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41a453b --- /dev/null +++ b/.gitignore @@ -0,0 +1,348 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# configuration +Web.Release.config +Web.Debug.config + +# project +package-lock.json +deps/* +cache/* \ No newline at end of file diff --git a/ITurnstilePolicy.cs b/ITurnstilePolicy.cs new file mode 100644 index 0000000..4cb3591 --- /dev/null +++ b/ITurnstilePolicy.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace TurnstileTag; + +/// +/// A marker interface for filters which define a policy for turnstile validation. +/// +public interface ITurnstilePolicy : IFilterMetadata +{ +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d6c2e52 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,24 @@ +Copyright (c) 2024 Tobias Klika + +MIT License + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..09e27b3 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +turnstile-tag \ No newline at end of file diff --git a/Turnstile.cs b/Turnstile.cs new file mode 100644 index 0000000..b25b199 --- /dev/null +++ b/Turnstile.cs @@ -0,0 +1,139 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Options; +using System.Net.Http.Json; +using System.Text.Json.Serialization; + +namespace TurnstileTag; + +public interface ITurnstile +{ + IHtmlContent GenerateTurnstileFormHtml(ViewContext viewContext); + IHtmlContent GetHtml(); + Task ValidateRequestAsync(HttpContext httpContext); + Task Verify(string value); +} + + +public class Turnstile : ITurnstile +{ + private readonly TurnstileOptions _options; + + + public Turnstile(IOptions options) + { + _options = options.Value; + } + + + public async Task ValidateRequestAsync(HttpContext httpContext) + { + ArgumentNullException.ThrowIfNull(httpContext); + + string[] allowed = new string[] { HttpMethods.Get, HttpMethods.Head, HttpMethods.Options, HttpMethods.Trace }; + + if (allowed.Contains(httpContext.Request.Method) || !httpContext.Request.HasFormContentType) + { + return; + } + + IFormCollection form; + + try + { + form = await httpContext.Request.ReadFormAsync(); + } + catch (Exception ex) when (ex is InvalidDataException or IOException) + { + // ReadFormAsync can throw InvalidDataException if the form content is malformed. + // [or] Reading the request body (which happens as part of ReadFromAsync) may throw an exception if a client disconnects. + // Wrap it in an AntiforgeryValidationException and allow the caller to handle it as just another antiforgery failure. + throw new TurnstileException("Unable to read the turnstile data from the posted form.", ex); + } + + string? captchaResponse = form[_options.FormFieldName].FirstOrDefault(); + + if (string.IsNullOrEmpty(captchaResponse) || !await Verify(captchaResponse)) + { + throw new TurnstileException("Could not verify captcha."); + } + } + + + public async Task Verify(string value) + { + using HttpClient http = new(); + using HttpResponseMessage response = await http.PostAsJsonAsync( + requestUri: new Uri(_options.ApiUrl + "/siteverify", UriKind.Absolute), + value: new Request(_options.SecretKey!, value) + ); + + Response? model = await response.Content.ReadFromJsonAsync(); + return model != null && model.Success; + } + + + /// + /// Generates the placeholder which renders the turnstile challenge. + /// + public IHtmlContent GetHtml() + { + HtmlContentBuilder builder = new(); + builder.AppendHtml($"
"); + builder.AppendHtml($""); + // + //
+ return builder; + } + + + public IHtmlContent GenerateTurnstileFormHtml(ViewContext viewContext) + { + ArgumentNullException.ThrowIfNull(viewContext); + + var formContext = viewContext.FormContext; + if (formContext.CanRenderAtEndOfForm) + { + if (formContext.FormData.ContainsKey("turnstile")) + { + return HtmlString.Empty; + } + + formContext.FormData.Add("turnstile", true); + } + + return GetHtml(); + } + + + class Request + { + public string Secret { get; set; } + + public string Response { get; set; } + + public Request(string secret, string response) + { + Secret = secret; + Response = response; + } + } + + class Response + { + public bool Success { get; set; } + + [JsonPropertyName("challenge_ts")] + public DateTimeOffset ChallengedDate { get; set; } + + public string? Hostname { get; set; } + + [JsonPropertyName("error-codes")] + public string[]? ErrorCodes { get; set; } + + public string? Action { get; set; } + + public string? Cdata { get; set; } + } +} \ No newline at end of file diff --git a/TurnstileException.cs b/TurnstileException.cs new file mode 100644 index 0000000..ffe938f --- /dev/null +++ b/TurnstileException.cs @@ -0,0 +1,21 @@ +namespace TurnstileTag; + + +/// +/// The that is thrown when the turnstile validation fails. +/// +public class TurnstileException : Exception +{ + /// + /// Creates a new instance of with the specified exception message. + /// + /// The message that describes the error. + public TurnstileException(string message) : base(message) { } + + /// + /// Creates a new instance of with the specified exception message and inner exception. + /// + /// The message that describes the error. + /// The inner . + public TurnstileException(string message, Exception innerException) : base(message, innerException) { } +} \ No newline at end of file diff --git a/TurnstileOptions.cs b/TurnstileOptions.cs new file mode 100644 index 0000000..9fa6b6c --- /dev/null +++ b/TurnstileOptions.cs @@ -0,0 +1,12 @@ +namespace TurnstileTag; + +public class TurnstileOptions +{ + public string? PublicKey { get; set; } + + public string? SecretKey { get; set; } + + public string ApiUrl { get; set; } = "https://challenges.cloudflare.com/turnstile/v0"; + + public string FormFieldName { get; set; } = "cf-turnstile-response"; +} \ No newline at end of file diff --git a/TurnstileServiceCollectionExtensions.cs b/TurnstileServiceCollectionExtensions.cs new file mode 100644 index 0000000..6a6d186 --- /dev/null +++ b/TurnstileServiceCollectionExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace TurnstileTag; + +public static class TurnstileServiceCollectionExtensions +{ + public static IServiceCollection AddTurnstile(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.AddOptions().BindConfiguration("Turnstile"); + return services; + } + + + public static IServiceCollection AddTurnstile(this IServiceCollection services, Action configure) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.AddOptions().BindConfiguration("Turnstile"); + services.PostConfigure(configure); + return services; + } + + + public static IServiceCollection AddTurnstile(this IServiceCollection services, string configName, IConfiguration namedConfigurationSection) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.AddOptions(configName).Bind(namedConfigurationSection); + return services; + } +} diff --git a/TurnstileTag.csproj b/TurnstileTag.csproj new file mode 100644 index 0000000..0a60523 --- /dev/null +++ b/TurnstileTag.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/TurnstileTag.sln b/TurnstileTag.sln new file mode 100644 index 0000000..55fb7c7 --- /dev/null +++ b/TurnstileTag.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34309.116 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TurnstileTag", "TurnstileTag.csproj", "{BA2FD0D8-1752-407E-ACF8-4697F641EA09}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BA2FD0D8-1752-407E-ACF8-4697F641EA09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA2FD0D8-1752-407E-ACF8-4697F641EA09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA2FD0D8-1752-407E-ACF8-4697F641EA09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA2FD0D8-1752-407E-ACF8-4697F641EA09}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3AFB6701-A6ED-4390-9E21-0EFFAE95AC3C} + EndGlobalSection +EndGlobal diff --git a/TurnstileTagHelper.cs b/TurnstileTagHelper.cs new file mode 100644 index 0000000..1216f85 --- /dev/null +++ b/TurnstileTagHelper.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace TurnstileTag; + +[HtmlTargetElement("form", Attributes = "cf-turnstile")] +public class TurnstileTagHelper : TagHelper +{ + readonly ITurnstile _turnstile; + + public TurnstileTagHelper(ITurnstile turnstile) + { + _turnstile = turnstile; + } + + /// + /// Whether turnstile should be activated. + /// + public bool? AppTurnstile { get; set; } + + /// + /// Gets the of the executing view. + /// + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext? ViewContext { get; set; } + + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (AppTurnstile ?? false) + { + IHtmlContent content = _turnstile.GenerateTurnstileFormHtml(ViewContext!); + if (content != null) + { + output.PostContent.AppendHtml(content); + } + } + } +} \ No newline at end of file diff --git a/ValidateTurnstileAttribute.cs b/ValidateTurnstileAttribute.cs new file mode 100644 index 0000000..fe0b6fe --- /dev/null +++ b/ValidateTurnstileAttribute.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace TurnstileTag; + +/// +/// Specifies that the class or method that this attribute is applied validates the turnstile response. +/// If the turnstile response is not available or invalid, the validation will fail +/// and the action method will not execute. +/// +/// +/// This attribute helps defend against bots and non-human requests. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public class ValidateTurnstileAttribute : Attribute, IFilterFactory, IOrderedFilter +{ + /// + public int Order { get; set; } = 1100; + + /// + public bool IsReusable => true; + + /// + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredService(); + } +} diff --git a/ValidateTurnstileAuthorizationFilter.cs b/ValidateTurnstileAuthorizationFilter.cs new file mode 100644 index 0000000..d067166 --- /dev/null +++ b/ValidateTurnstileAuthorizationFilter.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace TurnstileTag; + +internal partial class ValidateTurnstileAuthorizationFilter : IAsyncAuthorizationFilter, ITurnstilePolicy +{ + readonly ITurnstile _turnstile; + readonly ILogger _logger; + + public ValidateTurnstileAuthorizationFilter(ITurnstile turnstile, ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(turnstile); + + _turnstile = turnstile; + _logger = loggerFactory.CreateLogger(GetType()); + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + ArgumentNullException.ThrowIfNull(context); + + if (!context.IsEffectivePolicy(this)) + { + Log.NotMostEffectiveFilter(_logger, typeof(ITurnstilePolicy)); + return; + } + + if (!ShouldValidate(context)) + { + return; + } + + try + { + await _turnstile.ValidateRequestAsync(context.HttpContext); + } + catch (AntiforgeryValidationException exception) + { + Log.TurnstileInvalid(_logger, exception.Message, exception); + context.Result = new AntiforgeryValidationFailedResult(); + } + } + + protected virtual bool ShouldValidate(AuthorizationFilterContext context) + { + ArgumentNullException.ThrowIfNull(context); + return true; + } + + static partial class Log + { + [LoggerMessage(1, LogLevel.Information, "Turnstile validation failed. {Message}", EventName = "TurnstileInvalid")] + public static partial void TurnstileInvalid(ILogger logger, string message, Exception exception); + + [LoggerMessage(2, LogLevel.Trace, "Skipping the execution of current filter as its not the most effective filter implementing the policy {FilterPolicy}.", EventName = "NotMostEffectiveFilter")] + public static partial void NotMostEffectiveFilter(ILogger logger, Type filterPolicy); + } +}