Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a884c85
Add CAPTCHA solving plugin with CapSolver integration
kent13n Nov 23, 2025
3ee1c9e
Refactor CapSolver API to support vendor-specific task creation
kent13n Nov 23, 2025
5602278
Add `GoogleOptions` to support configurable Google reCAPTCHA solving
kent13n Nov 23, 2025
857c438
Add support for hCaptcha detection and CapSolver integration
kent13n Nov 23, 2025
40a4a33
Add support for Cloudflare Turnstile CAPTCHA detection and solving
kent13n Nov 23, 2025
d0a469f
Rename Cloudflare Turnstile methods for consistent CAPTCHA terminology
kent13n Nov 23, 2025
cb0f824
Update CaptchaSolverPlugin documentation for new providers and options
kent13n Nov 23, 2025
5b5f289
Refactor CAPTCHA handlers and plugin to simplify method signatures an…
kent13n Nov 23, 2025
084c578
Refactor CAPTCHA handling: unify payload structure, standardize metho…
kent13n Nov 24, 2025
58c26e9
Add GeeTest CAPTCHA handling and CapSolver integration
kent13n Nov 24, 2025
cab8cbf
Add TwoCaptcha provider to CaptchaSolver plugin
kent13n Nov 24, 2025
f4ab8ed
Add GeeTest v4 support, refactor handlers, and enhance test coverage
kent13n Nov 25, 2025
25c6bbb
Rename RecaptchaTests to GoogleTests and enhance test coverage
kent13n Nov 25, 2025
ad2b4f3
Add TwoCaptcha test coverage and enhance request handling logic
kent13n Nov 25, 2025
e9cba1a
Refactor CAPTCHA solving logic: add `FindCaptchaAsync`, improve error…
kent13n Nov 25, 2025
976736b
Update hCaptcha tests for TwoCaptcha and CapSolver: replace `SolveCap…
kent13n Nov 25, 2025
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
61 changes: 61 additions & 0 deletions PuppeteerExtraSharp/Plugins/CaptchaSolver/ApiClient/ApiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using PuppeteerExtraSharp.Utils;
namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.ApiClient;

public class ApiClient
{
private readonly HttpClient _client;

public ApiClient(string baseUrl = null)
{
_client = new HttpClient();

if (string.IsNullOrWhiteSpace(baseUrl)) return;

_client.BaseAddress = new Uri(baseUrl);
}

public PollingRequest<T> CreatePostPollingRequest<T>(string url, object content)
{
return new PollingRequest<T>(_client, () =>
{
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = JsonContent.Create(content);
return request;
});
}

public async Task<T?> PostAsync<T>(
string url,
object content,
CancellationToken token = default)
{
var data = JsonContent.Create(content);
var response = await _client.PostAsync(url, data, token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: token);
}

private Uri CreateUri(string url, Dictionary<string, string> query)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var fullUri))
{
if (_client.BaseAddress == null)
throw new InvalidOperationException("BaseAddress is not initialized");

fullUri = new Uri(_client.BaseAddress, url);
}

var uriBuilder = new UriBuilder(fullUri)
{
Query = QueryHelper.ToQuery(query)
};

return uriBuilder.Uri;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.ApiClient;

public class PollingRequest<T>(HttpClient client, Func<HttpRequestMessage> requestFactory)
{
private const int DefaultPollIntervalSeconds = 5;
private const int DefaultMaxAttempts = 5;

private int _timeout = DefaultPollIntervalSeconds;
private int _limit = DefaultMaxAttempts;

public PollingRequest<T> WithTimeoutSeconds(int timeout)
{
if (timeout <= 0) throw new ArgumentOutOfRangeException(nameof(timeout), "Timeout must be positive.");
_timeout = timeout;
return this;
}

public PollingRequest<T> TriesLimit(int limit)
{
if (limit <= 0) throw new ArgumentOutOfRangeException(nameof(limit), "Limit must be positive.");
_limit = limit;
return this;
}

public async Task<ApiResponse<T>> ActivatePollingAsync(
Func<ApiResponse<T>, PollingAction> decide, CancellationToken cancellation)
{
while (true)
{
cancellation.ThrowIfCancellationRequested();

using var request = requestFactory();
using var response = await client.SendAsync(request, cancellation).ConfigureAwait(false);

var apiResponse = new ApiResponse<T>
{
Data = await response.Content.ReadFromJsonAsync<T>(cancellationToken: cancellation).ConfigureAwait(false),
StatusCode = response.StatusCode
};

if (decide(apiResponse) == PollingAction.Break || _limit <= 1)
return apiResponse;

_limit -= 1;
await Task.Delay(TimeSpan.FromSeconds(_timeout), cancellation).ConfigureAwait(false);
}
}
}

public class ApiResponse<T>
{
public T? Data { get; set; }
public System.Net.HttpStatusCode StatusCode { get; set; }
}

public enum PollingAction
{
ContinuePolling,
Break
}
74 changes: 74 additions & 0 deletions PuppeteerExtraSharp/Plugins/CaptchaSolver/CaptchaSolverHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums;
using PuppeteerExtraSharp.Plugins.CaptchaSolver.Helpers;
using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces;
using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models;
using PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers;
using PuppeteerExtraSharp.Utils;
using PuppeteerSharp;
namespace PuppeteerExtraSharp.Plugins.CaptchaSolver;

public class CaptchaSolverHandler(ICaptchaSolverProvider provider, CaptchaSolverOptions options) : ICaptchaSolverHandler
{
private readonly CaptchaSolverOptions _options = options ?? new CaptchaSolverOptions();
private ICaptchaVendorHandler? _activeHandler;

public CaptchaVendor? Vendor { get; }
public async Task<bool> WaitForCaptchasAsync(IPage page, TimeSpan timeout)
{
foreach (var (vendor, options) in _options.EnabledVendors)
{
var handler = Helpers.Helpers.CreateHandler(vendor, provider, _options, page);
if (handler is null)
continue;

var handled = await handler.WaitForCaptchasAsync(timeout);
if (handled)
{
// Support only one active handler at a time
_activeHandler = handler;
return true;
}
}

return false;
}

public async Task<CaptchaResponse> FindCaptchasAsync(IPage page)
{
if (_activeHandler is null)
{
return new CaptchaResponse
{
Error = "No active captcha handler found",
};
}

await LoadScriptAsync(page, _options);
return await _activeHandler.FindCaptchasAsync();
}

public async Task<ICollection<CaptchaSolution>> SolveCaptchasAsync(IPage page, ICollection<Captcha> captchas)
{
await LoadScriptAsync(page, _options);
return await _activeHandler.SolveCaptchasAsync(captchas);
}

public async Task<EnterCaptchaSolutionsResult> EnterCaptchaSolutionsAsync(IPage page, ICollection<CaptchaSolution> solutions)
{
return await _activeHandler.EnterCaptchaSolutionsAsync(solutions);
}

private Task LoadScriptAsync(IPage page, params object[] args)
{
if (_activeHandler is null)
return Task.CompletedTask;

var recaptchaScriptName = $"{GetType().Namespace}.Vendors.{_activeHandler.Vendor}.{_activeHandler.Vendor}Script.js";
var script = ResourcesReader.ReadFile(recaptchaScriptName);
return page.EnsureEvaluateFunctionAsync(recaptchaScriptName, script, args);
}
}
149 changes: 149 additions & 0 deletions PuppeteerExtraSharp/Plugins/CaptchaSolver/CaptchaSolverPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums;
using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces;
using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models;
using PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers;
using PuppeteerSharp;
namespace PuppeteerExtraSharp.Plugins.CaptchaSolver;

public class CaptchaSolverPlugin : PuppeteerExtraPlugin
{
private readonly ICaptchaSolverProvider _provider;
private readonly ICaptchaSolverHandler _handler;
private readonly CaptchaSolverOptions _defaultOptions;

public CaptchaSolverPlugin(
ICaptchaSolverProvider provider, // Capsolver, TwoCaptcha
CaptchaSolverOptions options = null,
ICaptchaSolverHandler handler = null) : base("captcha-solver")
{
_defaultOptions = options ?? new CaptchaSolverOptions();
_handler = handler ?? new CaptchaSolverHandler(provider, _defaultOptions);
_provider = provider;
}

public async Task<CaptchaResponse> FindCaptchaAsync(IPage page, CaptchaSolverOptions optionsOverride = null)
{
var options = optionsOverride ?? _defaultOptions;

var hasCaptchas = await _handler.WaitForCaptchasAsync(page, options.CaptchaWaitTimeout);

if (!hasCaptchas)
{
return new CaptchaResponse
{
Captchas = [],
Error = "No captchas found"
};
}

return await _handler.FindCaptchasAsync(page);
}

public async Task<EnterCaptchaSolutionsResult> SolveCaptchaAsync(IPage page,
CaptchaSolverOptions optionsOverride = null)
{
var options = optionsOverride ?? _defaultOptions;

var captchaResponse = await FindCaptchaAsync(page, options);

if (!captchaResponse.Captchas.Any())
{
return new EnterCaptchaSolutionsResult()
{
Error = "No captchas found"
};
}

if (options.ThrowOnError && !string.IsNullOrWhiteSpace(captchaResponse.Error))
{
throw new CaptchaException(page.Url, captchaResponse.Error);
}

var filteredCaptchas = FilterCaptchas(captchaResponse.Captchas, options);

var captchas = filteredCaptchas.unfiltered.ToList();
if (captchas.Count == 0)
{
return new EnterCaptchaSolutionsResult()
{
Error = "No captchas found or all captchas have been filtered",
Filtered = filteredCaptchas.filtered,
};
}

var solvedCaptchas = await _handler.SolveCaptchasAsync(page, captchas);
var result = await _handler.EnterCaptchaSolutionsAsync(page, solvedCaptchas);
result.Filtered = filteredCaptchas.filtered;

if (options.ThrowOnError && !string.IsNullOrWhiteSpace(result.Error))
{
throw new CaptchaException(page.Url, result.Error);
}

return result;
}

protected internal override async Task OnPageCreatedAsync(IPage page)
{
await page.SetBypassCSPAsync(true);
foreach (var vendor in _defaultOptions.EnabledVendors.Keys)
{
var handler = Helpers.Helpers.CreateHandler(vendor, _provider, _defaultOptions, page);
if (handler is null)
continue;

_ = handler.HandleOnPageCreatedAsync();
}
}

private (ICollection<Captcha> unfiltered, ICollection<FilteredCaptcha> filtered) FilterCaptchas(ICollection<Captcha> captchas, CaptchaSolverOptions options)
{
var filteredCaptchas = new List<FilteredCaptcha>();
var unfilteredCaptchas = new List<Captcha>();
foreach (var captcha in captchas)
{
switch (captcha.CaptchaType)
{
case CaptchaType.invisible when !options.SolveInvisibleChallenges:
filteredCaptchas.Add(new FilteredCaptcha()
{
Captcha = captcha,
FilteredReason = "solveInvisibleChallenges"
});
continue;
case CaptchaType.invisible when
!captcha.HasActiveChallengePopup &&
!options.SolveInactiveChallenges:
filteredCaptchas.Add(new FilteredCaptcha()
{
Captcha = captcha,
FilteredReason = "solveInactiveChallenges"
});
continue;
case CaptchaType.score when !options.SolveScoreBased:
filteredCaptchas.Add(new FilteredCaptcha()
{
Captcha = captcha,
FilteredReason = "solveScoreBased"
});
continue;
case CaptchaType.checkbox when !captcha.IsInViewport && options.SolveInViewportOnly:
filteredCaptchas.Add(new FilteredCaptcha()
{
Captcha = captcha,
FilteredReason = "solveInViewportOnly"
});
continue;
default:
unfilteredCaptchas.Add(captcha);
break;
}
}

return (unfilteredCaptchas, filteredCaptchas);
}
}
10 changes: 10 additions & 0 deletions PuppeteerExtraSharp/Plugins/CaptchaSolver/Enums/CaptchaVendor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums;

public enum CaptchaVendor
{
Google,
HCaptcha,
DataDome,
Cloudflare,
GeeTest
}
16 changes: 16 additions & 0 deletions PuppeteerExtraSharp/Plugins/CaptchaSolver/Enums/CaptchaVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums;

public enum CaptchaVersion
{
RecaptchaV2,
RecaptchaV3,
GeeTestV3,
GeeTestV4,
Turnstile,

// TODO: not supported yet
HCaptcha,

// TODO: not supported yet
DataDome,
}
Loading