Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BitArmory.ReCaptcha.Tests/BitArmory.ReCaptcha.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
<ItemGroup>
<ProjectReference Include="..\BitArmory.ReCaptcha\BitArmory.ReCaptcha.csproj" />
</ItemGroup>
</Project>
</Project>
134 changes: 133 additions & 1 deletion BitArmory.ReCaptcha.Tests/CaptchaVersion2Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ string ResponseJson(bool isSuccess)
return json;
}

string ResponseJsonWithHostAndAction(bool isSuccess, string hostname, string action)
{
var json = @"{
""success"": {SUCCESS},
""challenge_ts"": ""2018-05-15T23:05:22Z"",
""hostname"": ""{HOSTNAME}"",
""action"": ""{ACTION}""
}"
.Replace("{SUCCESS}", isSuccess.ToString().ToLower())
.Replace("{HOSTNAME}", hostname)
.Replace("{ACTION}", action);

return json;
}

[Test]
public async Task can_verify_a_captcha()
{
Expand Down Expand Up @@ -60,5 +75,122 @@ public async Task can_verify_failed_response()

mockHttp.VerifyNoOutstandingExpectation();
}

[Test]
public async Task can_verify_a_captcha_from_another_url()
{
var responseJson = ResponseJson(true);

var mockHttp = new MockHttpMessageHandler();

mockHttp.Expect(HttpMethod.Post, Constants.TurnstileVerifyUrl)
.Respond("application/json", responseJson)
.WithExactFormData("response=aaaaa&remoteip=bbbb&secret=cccc");

var captcha = new ReCaptchaService(Constants.TurnstileVerifyUrl, client: mockHttp.ToHttpClient());

var response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc");

response.Should().BeTrue();

mockHttp.VerifyNoOutstandingExpectation();
}

[Test]
public async Task can_verify_a_captcha_with_hostname_and_action()
{
var responseJson = ResponseJson(true);
var mockHttp = new MockHttpMessageHandler();
mockHttp.When(HttpMethod.Post, Constants.TurnstileVerifyUrl)
.Respond("application/json", responseJson)
.WithExactFormData("response=aaaaa&remoteip=bbbb&secret=cccc");
var captcha = new ReCaptchaService(Constants.TurnstileVerifyUrl, client: mockHttp.ToHttpClient());

var response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", hostname: "localhost");
response.Should().BeTrue();

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", hostname: "not-localhost");
response.Should().BeFalse();

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", hostname: "not-localhost", action: "test");
response.Should().BeFalse();

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", hostname: "localhost", action: "test");
response.Should().BeFalse();

mockHttp.VerifyNoOutstandingExpectation();

// Now generate responses with the action field filled out (and a different hostname)
responseJson = ResponseJsonWithHostAndAction(true, "example.com", "test-action");
mockHttp = new MockHttpMessageHandler();
mockHttp.When(HttpMethod.Post, Constants.TurnstileVerifyUrl)
.Respond("application/json", responseJson)
.WithExactFormData("response=aaaaa&remoteip=bbbb&secret=cccc");
captcha = new ReCaptchaService(Constants.TurnstileVerifyUrl, client: mockHttp.ToHttpClient());

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", hostname: "localhost", action: "test-action");
response.Should().BeFalse();

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", hostname: "example.com", action: "test-action");
response.Should().BeTrue();

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", hostname: "example.com", action: "action");
response.Should().BeFalse();

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", hostname: "example.com");
response.Should().BeTrue();

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", action: "example.com");
response.Should().BeFalse();

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc", action: "test-action");
response.Should().BeTrue();

response = await captcha.Verify2Async("aaaaa", "bbbb", "cccc");
response.Should().BeTrue();

mockHttp.VerifyNoOutstandingExpectation();
}

[Test]
public async Task can_parse_full_v2_response()
{
var responseJson = @"{
""success"": true,
""challenge_ts"": ""2018-05-15T23:05:22Z"",
""hostname"": ""example.net"",
""action"": ""test-action"",
""apk_package_name"": ""test-package"",
""cdata"": ""customer data""
}";
var mockHttp = new MockHttpMessageHandler();
mockHttp.Expect(HttpMethod.Post, Constants.TurnstileVerifyUrl)
.Respond("application/json", responseJson)
.WithExactFormData("response=aaaaa&remoteip=bbbb&secret=cccc");
var captcha = new ReCaptchaService(Constants.TurnstileVerifyUrl, client: mockHttp.ToHttpClient());

var response = await captcha.Response2Async("aaaaa", "bbbb", "cccc");
response.IsSuccess.Should().BeTrue();
response.ChallengeTs.Should().Be("2018-05-15T23:05:22Z");
response.HostName.Should().Be("example.net");
response.Action.Should().Be("test-action");
response.ApkPackageName.Should().Be("test-package");
response.CData.Should().Be("customer data");

responseJson = ResponseJson(false);
mockHttp = new MockHttpMessageHandler();
mockHttp.Expect(HttpMethod.Post, Constants.VerifyUrl)
.Respond("application/json", responseJson)
.WithExactFormData("response=aaaaa&remoteip=bbbb&secret=cccc");
captcha = new ReCaptchaService(client: mockHttp.ToHttpClient());

response = await captcha.Response2Async("aaaaa", "bbbb", "cccc");
response.IsSuccess.Should().BeFalse();
response.ChallengeTs.Should().Be("2018-05-15T23:05:22Z");
response.HostName.Should().Be("localhost");
response.Action.Should().Be(null);
response.ApkPackageName.Should().Be(null);
response.CData.Should().Be(null);
}
}
}
}
6 changes: 3 additions & 3 deletions BitArmory.ReCaptcha/BitArmory.ReCaptcha.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PackageReleaseNotes>
<Version>0.0.0-localbuild</Version>
<Authors>Brian Chavez</Authors>
<TargetFrameworks>net45;netstandard1.3;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net6.0;net45;netstandard1.3;netstandard2.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<CodeAnalysisRuleSet>BitArmory.ReCaptcha.ruleset</CodeAnalysisRuleSet>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand All @@ -14,7 +14,7 @@
<SignAssembly>false</SignAssembly>
<AssemblyName>BitArmory.ReCaptcha</AssemblyName>
<PackageId>BitArmory.ReCaptcha</PackageId>
<PackageTags>google;recaptcha;captcha;asp.net;aspnet;mvc;core;razor;razorpages;webforms;security;bot;anti-spam;validation;recpatcha</PackageTags>
<PackageTags>google;recaptcha;captcha;cloudflare;turnstile;asp.net;aspnet;mvc;core;razor;razorpages;webforms;security;bot;anti-spam;validation;recpatcha</PackageTags>
<PackageIconUrl>https://raw.githubusercontent.com/BitArmory/ReCaptcha/master/docs/recaptcha.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/BitArmory/ReCaptcha</PackageProjectUrl>
<PackageLicenseUrl>https://raw.githubusercontent.com/BitArmory/ReCaptcha/master/LICENSE</PackageLicenseUrl>
Expand All @@ -40,4 +40,4 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
<Reference Include="System.Net.Http" />
</ItemGroup>
</Project>
</Project>
26 changes: 23 additions & 3 deletions BitArmory.ReCaptcha/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,33 @@ public static class Constants
public const string VerifyUrl = "https://www.google.com/recaptcha/api/siteverify";

/// <summary>
/// Default URL for reCAPTCHA.js
/// Default URL for verifying reCAPTCHA.
/// </summary>
public const string TurnstileVerifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify";

/// <summary>
/// Default JavaScript URL for reCAPTCHA.js
/// </summary>
public const string JavaScriptUrl = "https://www.google.com/recaptcha/api.js";

/// <summary>
/// <summary>
/// More available JavaScript URL for reCAPTCHA.js (available where google.com isn't)
/// </summary>
public const string NonGoogleJavaScriptUrl = "https://www.recaptcha.net/recaptcha/api.js";

/// <summary>
/// Default JavaScript URL for Cloudflare Turnstile
/// </summary>
public const string TurnstileJavaScriptUrl = "https://challenges.cloudflare.com/turnstile/v0/api.js";

/// <summary>
/// Default HTTP header key for reCAPTCHA response.
/// </summary>
public const string ClientResponseKey = "g-recaptcha-response";

/// <summary>
/// Default HTTP header key for reCAPTCHA response.
/// </summary>
public const string TurnstileClientResponseKey = "cf-turnstile-response";
}
}
}
59 changes: 56 additions & 3 deletions BitArmory.ReCaptcha/ReCaptchaResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,55 @@ public class JsonResponse
}

/// <summary>
/// Response from reCAPTCHA verify URL.
/// Response from reCAPTCHA v2 or Cloudflare Turnstile verify URL.
/// </summary>
public class ReCaptcha2Response : JsonResponse
{
/// <summary>
/// Whether this request was a valid reCAPTCHA token for your site.
/// </summary>
public bool IsSuccess { get; set; }

/// <summary>
/// The action name for this request, only provided by Cloudflare Turnstile, not reCAPTCHA v2 (important to verify).
/// </summary>
public string Action { get; set; }

/// <summary>
/// Timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ).
/// </summary>
public string ChallengeTs { get; set; }

/// <summary>
/// missing-input-secret: The secret parameter is missing.
/// invalid-input-secret: The secret parameter is invalid or malformed.
/// missing-input-response: The response parameter is missing.
/// invalid-input-response: The response parameter is invalid or malformed.
/// bad-request: The request is invalid or malformed.
/// timeout-or-duplicate: The response is no longer valid: either is too old or has been used previously.
/// </summary>
public string[] ErrorCodes { get; set; }

/// <summary>
/// The hostname of the site where the reCAPTCHA was solved (if solved on a website).
/// </summary>
public string HostName { get; set; }

/// <summary>
/// ApkPackageName is the package name of the app the captcha was solved on (if solved in an android app).
/// </summary>
public string ApkPackageName { get; set; }

/// <summary>
/// Customer data passed on the client side.
///
/// Only provided by Cloudflare Turnstile, not reCAPTCHA v2.
/// </summary>
public string CData { get; set; }
}

/// <summary>
/// Response from reCAPTCHA v3 verify URL.
/// </summary>
public class ReCaptcha3Response : JsonResponse
{
Expand Down Expand Up @@ -49,9 +97,14 @@ public class ReCaptcha3Response : JsonResponse
public string[] ErrorCodes { get; set; }

/// <summary>
/// The hostname of the site where the reCAPTCHA was solved
/// The hostname of the site where the reCAPTCHA was solved (if solved on a website).
/// </summary>
public string HostName { get; set; }

/// <summary>
/// ApkPackageName is the package name of the app the captcha was solved on (if solved in an android app).
/// </summary>
public string ApkPackageName { get; set; }
}

}
}
Loading