Files
myAi/api/Services/RecaptchaVerifier.cs
T
claude fa1ef23c02
Build and Push Docker Images / build (push) Successful in 37s
Changes
2026-05-04 21:02:35 +03:00

106 lines
4.4 KiB
C#

using Api.Services.Contracts.Models;
using Api.Services.Contracts;
using Api.Settings;
using Microsoft.Extensions.Options;
namespace Api.Services
{
public sealed class RecaptchaVerifier : ICaptchaVerifier
{
private readonly HttpClient _http;
private readonly CaptchaSettings _opt;
private readonly ILogger<RecaptchaVerifier> _log;
public RecaptchaVerifier(HttpClient http, IOptions<CaptchaSettings> options, ILogger<RecaptchaVerifier> log)
{
_http = http;
_opt = options.Value;
_log = log;
}
public async Task<CaptchaVerdictModel> VerifyAsync(string token, string? userIp, CancellationToken ct)
{
_log.LogDebug("Verifying captcha token for IP {Ip}", userIp ?? "unknown");
if (string.IsNullOrWhiteSpace(_opt.SecretKey))
{
_log.LogWarning("Captcha verification attempted but SecretKey is not configured");
return new CaptchaVerdictModel(false, "Captcha not configured", null);
}
var form = new Dictionary<string, string>
{
["secret"] = _opt.SecretKey,
["response"] = token
};
if (!string.IsNullOrWhiteSpace(userIp))
form["remoteip"] = userIp;
using var resp = await _http.PostAsync(
"https://www.google.com/recaptcha/api/siteverify",
new FormUrlEncodedContent(form),
ct
);
if (!resp.IsSuccessStatusCode)
{
_log.LogWarning("Captcha HTTP request failed with status {StatusCode} for IP {Ip}",
(int)resp.StatusCode, userIp ?? "unknown");
return new CaptchaVerdictModel(false, $"Captcha HTTP {(int)resp.StatusCode}", null);
}
var data = await resp.Content.ReadFromJsonAsync<RecaptchaResponse>(cancellationToken: ct);
if (data is null)
{
_log.LogError("Failed to parse captcha response for IP {Ip}", userIp ?? "unknown");
return new CaptchaVerdictModel(false, "Captcha parse error", null);
}
if (!data.success)
{
_log.LogWarning("Captcha verification failed for IP {Ip}. Score={Score}",
userIp ?? "unknown", data.score);
return new CaptchaVerdictModel(false, "Captcha failed", data.score);
}
// v3 score check (score is typically null for v2)
if (data.score is double score && score < _opt.MinimumScore)
{
_log.LogWarning("Captcha score {Score} below minimum {MinScore} for IP {Ip}",
score, _opt.MinimumScore, userIp ?? "unknown");
return new CaptchaVerdictModel(false, "Captcha score too low", score);
}
// Optional strictness (usually v3): action/hostname checks
if (!string.IsNullOrWhiteSpace(_opt.ExpectedAction) &&
!string.Equals(_opt.ExpectedAction, data.action, StringComparison.Ordinal))
{
_log.LogWarning("Captcha action mismatch. Expected={Expected}, Actual={Actual}, IP={Ip}",
_opt.ExpectedAction, data.action, userIp ?? "unknown");
return new CaptchaVerdictModel(false, "Captcha action mismatch", data.score);
}
if (!string.IsNullOrWhiteSpace(_opt.ExpectedHostname) &&
!string.Equals(_opt.ExpectedHostname, data.hostname, StringComparison.OrdinalIgnoreCase))
{
_log.LogWarning("Captcha hostname mismatch. Expected={Expected}, Actual={Actual}, IP={Ip}",
_opt.ExpectedHostname, data.hostname, userIp ?? "unknown");
return new CaptchaVerdictModel(false, "Captcha hostname mismatch", data.score);
}
_log.LogInformation("Captcha verified successfully for IP {Ip}. Score={Score}",
userIp ?? "unknown", data.score);
return new CaptchaVerdictModel(true, null, data.score);
}
private sealed class RecaptchaResponse
{
public bool success { get; set; }
public double? score { get; set; } // v3
public string? action { get; set; } // v3
public string? hostname { get; set; }
public DateTimeOffset? challenge_ts { get; set; }
}
}
}