Files
2024-04-05 13:03:12 +02:00

168 lines
4.1 KiB
C#

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
{
/// <summary>
/// Generates the placeholder which renders the turnstile challenge.
/// </summary>
public IHtmlContent GetInputHtml();
/// <summary>
/// Generates the script tag which loads the turnstile library.
/// </summary>
public IHtmlContent GetScriptHtml();
/// <summary>
///
/// </summary>
IHtmlContent GenerateTurnstileFormHtml(ViewContext viewContext);
/// <summary>
///
/// </summary>
Task ValidateRequestAsync(HttpContext httpContext);
/// <summary>
///
/// </summary>
Task<bool> Verify(string value);
}
public class Turnstile : ITurnstile
{
private readonly TurnstileOptions _options;
public Turnstile(IOptions<TurnstileOptions> options)
{
_options = options.Value;
}
/// <inheritdoc />
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.");
}
}
/// <inheritdoc />
public async Task<bool> 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<Response>();
return model != null && model.Success;
}
/// <inheritdoc />
public IHtmlContent GetInputHtml()
{
HtmlContentBuilder builder = new();
builder.AppendHtml($"<div class=\"cf-turnstile\" data-sitekey=\"{_options.PublicKey}\"></div>");
return builder;
}
/// <inheritdoc />
public IHtmlContent GetScriptHtml()
{
HtmlContentBuilder builder = new();
builder.AppendHtml($"<script src=\"{_options.ApiUrl}/api.js\" async defer></script>");
return builder;
}
/// <inheritdoc />
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);
}
viewContext.ViewData.TryAdd("turnstile", true);
return GetInputHtml();
}
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; }
}
}