├── PuppeteerExtraSharp ├── Plugins │ ├── PuppeteerExtraPluginOptions.cs │ ├── CaptchaSolver │ │ ├── Interfaces │ │ │ ├── ICaptchaSolveOptions.cs │ │ │ ├── ICaptchaSolverProvider.cs │ │ │ ├── ICaptchaSolver.cs │ │ │ └── ICaptchaVendor.cs │ │ ├── Models │ │ │ ├── CaptchaException.cs │ │ │ ├── CaptchaResponse.cs │ │ │ ├── CaptchaType.cs │ │ │ ├── CaptchaSolution.cs │ │ │ ├── FilteredCaptcha.cs │ │ │ ├── CaptchaDisplay.cs │ │ │ ├── EnterCaptchaSolutionsResult.cs │ │ │ ├── CaptchaSolved.cs │ │ │ ├── Captcha.cs │ │ │ └── CaptchaOptions.cs │ │ ├── Enums │ │ │ ├── CaptchaVendor.cs │ │ │ └── CaptchaVersion.cs │ │ ├── Vendors │ │ │ ├── GeeTest │ │ │ │ └── GeeTestInterceptorScript.js │ │ │ ├── Google │ │ │ │ ├── GoogleOptions.cs │ │ │ │ └── GoogleVendor.cs │ │ │ ├── Cloudflare │ │ │ │ └── CloudflareVendor.cs │ │ │ └── HCaptcha │ │ │ │ └── HCaptchaVendor.cs │ │ ├── Providers │ │ │ ├── CapSolver │ │ │ │ ├── Models │ │ │ │ │ └── CapSolverCreateTaskResponse.cs │ │ │ │ └── CapSolver.cs │ │ │ ├── TwoCaptcha │ │ │ │ ├── Models │ │ │ │ │ └── TwoCaptchaCreateTaskResponse.cs │ │ │ │ └── TwoCaptcha.cs │ │ │ ├── GetCaptchaSolutionRequest.cs │ │ │ └── CaptchaProviderOptions.cs │ │ ├── CaptchaOptionsScope.cs │ │ ├── Helpers │ │ │ └── Helpers.cs │ │ └── ApiClient │ │ │ ├── PollingRequest.cs │ │ │ └── ApiClient.cs │ ├── Recaptcha │ │ ├── Models │ │ │ ├── CaptchaSolution.cs │ │ │ ├── FilteredCaptcha.cs │ │ │ ├── CaptchaException.cs │ │ │ ├── CaptchaResponse.cs │ │ │ ├── CaptchaType.cs │ │ │ ├── CaptchaDisplay.cs │ │ │ ├── EnterCaptchaSolutionsResult.cs │ │ │ ├── CaptchaSolved.cs │ │ │ ├── Captcha.cs │ │ │ └── RecaptchaSolveOptions.cs │ │ ├── Provider │ │ │ ├── IRecaptchaProvider.cs │ │ │ ├── GetRecaptchaSolutionRequest.cs │ │ │ ├── CapSolver │ │ │ │ ├── Models │ │ │ │ │ └── CapSolverCreateTaskResponse.cs │ │ │ │ ├── CapSolver.cs │ │ │ │ └── CapSolverApi.cs │ │ │ ├── TwoCaptcha │ │ │ │ ├── Models │ │ │ │ │ └── TwoCaptchaCreateTaskResponse.cs │ │ │ │ ├── TwoCaptcha.cs │ │ │ │ └── TwoCaptchaApi.cs │ │ │ └── CaptchaProviderOptions.cs │ │ ├── ICaptchaHandler.cs │ │ ├── Helpers │ │ │ └── ScriptHelper.cs │ │ ├── ApiClient │ │ │ ├── ApiClient.cs │ │ │ └── PollingRequest.cs │ │ ├── readme.md │ │ ├── RecapchaPlugin.cs │ │ └── RecaptchaHandler.cs │ ├── BlockResources │ │ ├── Rules │ │ │ ├── IBlockRule.cs │ │ │ ├── ICondition.cs │ │ │ ├── PageCondition.cs │ │ │ ├── PredicateCondition.cs │ │ │ ├── PredicateBlockRule.cs │ │ │ ├── ResourceCondition.cs │ │ │ ├── UrlPatternCondition.cs │ │ │ ├── AggregateCondition.cs │ │ │ └── ResourcesBlockBuilder.cs │ │ ├── BlockResourcesPlugin.cs │ │ └── README.md │ ├── ExtraStealth │ │ ├── Scripts │ │ │ ├── Language.js │ │ │ ├── HardwareConcurrency.js │ │ │ ├── Vendor.js │ │ │ ├── Outdimensions.js │ │ │ ├── WebDriver.js │ │ │ ├── SCI.js │ │ │ ├── WebGL.js │ │ │ ├── Permissions.js │ │ │ ├── Stacktrace.js │ │ │ ├── Codec.js │ │ │ ├── ContentWindow.js │ │ │ └── LoadTimes.js │ │ ├── Evasions │ │ │ ├── Codec.cs │ │ │ ├── ChromeSci.cs │ │ │ ├── LoadTimes.cs │ │ │ ├── ChromeRuntime.cs │ │ │ ├── Permissions.cs │ │ │ ├── PluginEvasion.cs │ │ │ ├── StackTrace.cs │ │ │ ├── OutDimensions.cs │ │ │ ├── ContentWindow.cs │ │ │ ├── Vendor.cs │ │ │ ├── Languages.cs │ │ │ ├── HardwareConcurrency.cs │ │ │ ├── WebGl.cs │ │ │ ├── WebDriver.cs │ │ │ └── SourceUrl.cs │ │ ├── StealthUtils.cs │ │ ├── readme.md │ │ └── StealthPlugin.cs │ ├── AnonymizeUa │ │ └── AnonymizeUaPlugin.cs │ └── PuppeteerExtraPlugin.cs ├── BrowserStartContext.cs ├── Utils │ ├── QueryHelper.cs │ └── ResoursesReader.cs ├── PuppeteerExtraSharp.sln ├── README.md ├── PuppeteerExtraSharp.csproj ├── .gitignore └── PuppeteerExtra.cs ├── Tests ├── Constants.cs ├── Utils │ ├── JsonElementExtensions.cs │ └── FingerPrint.cs ├── ExtraLaunchTest.cs ├── StealthPluginTests │ ├── EvasionsTests │ │ ├── VendorTest.cs │ │ ├── StealthPluginTest.cs │ │ ├── UserAgentTest.cs │ │ ├── PluginTest.cs │ │ ├── PluginEvasionTest.cs │ │ ├── SourceUrl │ │ │ ├── fixtures │ │ │ │ └── Test.html │ │ │ └── SourceUrlTest.cs │ │ ├── WebDriverTest.cs │ │ ├── LanguagesTest.cs │ │ ├── PermissionsTest.cs │ │ ├── LoadTimesTest.cs │ │ ├── ChromeSciTest.cs │ │ ├── CodecTest.cs │ │ ├── RuntimeTest.cs │ │ └── ChromeAppTest.cs │ └── StealthPluginTests.cs ├── CaptchaSolverTests │ ├── CaptchaSolverTestsBase.cs │ ├── CloudflareTests.cs │ ├── HCaptchaTests.cs │ ├── GeeTestTests.cs │ └── GoogleTests.cs ├── Extra.Tests.csproj ├── BrowserDefault.cs ├── Recaptcha │ ├── RecaptchaTests.cs │ └── CapSolverRecaptchaTests.cs └── Properties │ └── Resources.Designer.cs ├── LICENSE ├── PuppeteerExtraSharp.sln └── README.md /PuppeteerExtraSharp/Plugins/PuppeteerExtraPluginOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins; 2 | 3 | public interface IPuppeteerExtraPluginOptions 4 | { 5 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Interfaces/ICaptchaSolveOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 2 | 3 | public interface ICaptchaSolveOptions 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /Tests/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Extra.Tests 2 | { 3 | public static class Constants 4 | { 5 | public static readonly string PathToChrome = @"chrome.exe"; 6 | public static readonly bool Headless = true; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/CaptchaSolution.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 2 | 3 | public class CaptchaSolution 4 | { 5 | public string Id { get; set; } 6 | public string Text { get; set; } 7 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/Rules/IBlockRule.cs: -------------------------------------------------------------------------------- 1 | using PuppeteerSharp; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.BlockResources.Rules; 4 | 5 | public interface IBlockRule 6 | { 7 | public bool ShouldBlock(IRequest request); 8 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/Rules/ICondition.cs: -------------------------------------------------------------------------------- 1 | using PuppeteerSharp; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.BlockResources.Rules; 4 | 5 | internal interface ICondition 6 | { 7 | public bool IsApplies(IRequest request); 8 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/FilteredCaptcha.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 2 | 3 | public class FilteredCaptcha 4 | { 5 | public Captcha Captcha { get; set; } 6 | public string FilteredReason { get; set; } 7 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/CaptchaException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 3 | 4 | public class CaptchaException(string url, string error) : Exception($"Error while solving CAPTCHA on {url}: {error}"); -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/CaptchaException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 4 | 5 | public class CaptchaException(string url, string error) : Exception($"Error while solving reCAPTCHA on {url}: {error}"); -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/Language.js: -------------------------------------------------------------------------------- 1 | (...languages) => { 2 | utils.replaceGetterWithProxy( 3 | Object.getPrototypeOf(navigator), 4 | 'languages', 5 | utils.makeHandler().getterValue(Object.freeze(languages)) 6 | ) 7 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/HardwareConcurrency.js: -------------------------------------------------------------------------------- 1 | (concurrency) => { 2 | 3 | utils.replaceGetterWithProxy( 4 | Object.getPrototypeOf(navigator), 5 | 'hardwareConcurrency', 6 | utils.makeHandler().getterValue(concurrency) 7 | ) 8 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/BrowserStartContext.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp; 2 | 3 | public class BrowserStartContext 4 | { 5 | public bool IsHeadless { get; set; } 6 | public StartType StartType { get; set; } 7 | } 8 | 9 | public enum StartType 10 | { 11 | Connect, 12 | Launch 13 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/CaptchaResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 4 | 5 | public class CaptchaResponse 6 | { 7 | public List Captchas { get; set; } 8 | public string Error { get; set; } 9 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/IRecaptchaProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 4 | 5 | public interface IRecaptchaProvider 6 | { 7 | public Task GetSolutionAsync(GetRecaptchaSolutionRequest request); 8 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/CaptchaResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 3 | 4 | public class CaptchaResponse 5 | { 6 | public List Captchas { get; set; } 7 | public string Error { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/Vendor.js: -------------------------------------------------------------------------------- 1 | (vendor) => { 2 | // Overwrite the `vendor` property to use a custom getter. 3 | utils.replaceGetterWithProxy( 4 | Object.getPrototypeOf(navigator), 5 | 'vendor', 6 | utils.makeHandler().getterValue(vendor) 7 | ) 8 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/CaptchaType.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum CaptchaType 7 | { 8 | invisible, 9 | checkbox, 10 | score, 11 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/CaptchaType.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum CaptchaType 7 | { 8 | invisible, 9 | checkbox, 10 | score, 11 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/Rules/PageCondition.cs: -------------------------------------------------------------------------------- 1 | using PuppeteerSharp; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.BlockResources.Rules; 4 | 5 | internal class PageCondition(IPage page) : ICondition 6 | { 7 | public bool IsApplies(IRequest request) 8 | { 9 | return page == request.Frame.Page; 10 | } 11 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Enums/CaptchaVendor.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum CaptchaVendor 7 | { 8 | Google, 9 | HCaptcha, 10 | DataDome, 11 | Cloudflare, 12 | GeeTest 13 | } 14 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Interfaces/ICaptchaSolverProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers; 3 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 4 | 5 | public interface ICaptchaSolverProvider 6 | { 7 | public Task GetSolutionAsync(GetCaptchaSolutionRequest request); 8 | } 9 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/CaptchaSolution.cs: -------------------------------------------------------------------------------- 1 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 4 | 5 | public class CaptchaSolution 6 | { 7 | public string Id { get; set; } 8 | public CaptchaVendor Vendor { get; set; } 9 | public string Payload { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/Rules/PredicateCondition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.BlockResources.Rules; 5 | 6 | public class PredicateCondition(Func condition) : ICondition 7 | { 8 | public bool IsApplies(IRequest request) 9 | { 10 | return condition(request); 11 | } 12 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/FilteredCaptcha.cs: -------------------------------------------------------------------------------- 1 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 4 | 5 | public class FilteredCaptcha 6 | { 7 | public CaptchaVendor Vendor { get; set; } 8 | public Captcha Captcha { get; set; } 9 | public string FilteredReason { get; set; } 10 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/Rules/PredicateBlockRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.BlockResources.Rules; 5 | 6 | internal class PredicateBlockRule(Func predicate) : IBlockRule 7 | { 8 | public bool ShouldBlock(IRequest request) 9 | { 10 | return predicate(request); 11 | } 12 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Enums/CaptchaVersion.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 2 | 3 | public enum CaptchaVersion 4 | { 5 | RecaptchaV2, 6 | RecaptchaV3, 7 | GeeTestV3, 8 | GeeTestV4, 9 | Turnstile, 10 | 11 | // TODO: not supported yet 12 | HCaptcha, 13 | 14 | // TODO: not supported yet 15 | DataDome, 16 | } 17 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/CaptchaDisplay.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 2 | 3 | public class CaptchaDisplay 4 | { 5 | public string Size { get; set; } 6 | public double Top { get; set; } 7 | public double Left { get; set; } 8 | public double Width { get; set; } 9 | public double Height { get; set; } 10 | public string Theme { get; set; } 11 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/Rules/ResourceCondition.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.BlockResources.Rules; 5 | 6 | internal class ResourceCondition(params ResourceType[] resourceTypes) : ICondition 7 | { 8 | public bool IsApplies(IRequest request) 9 | { 10 | return resourceTypes.Contains(request.ResourceType); 11 | } 12 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/Rules/UrlPatternCondition.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.BlockResources.Rules; 5 | 6 | internal class UrlPatternCondition(string urlPattern) : ICondition 7 | { 8 | public bool IsApplies(IRequest request) 9 | { 10 | return Regex.IsMatch(request.Url, urlPattern); 11 | } 12 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/CaptchaDisplay.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 2 | 3 | public class CaptchaDisplay 4 | { 5 | public string Size { get; set; } 6 | public double Top { get; set; } 7 | public double Left { get; set; } 8 | public double Width { get; set; } 9 | public double Height { get; set; } 10 | public string Theme { get; set; } 11 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/Outdimensions.js: -------------------------------------------------------------------------------- 1 | () => { 2 | try { 3 | if (window.outerWidth && window.outerHeight) { 4 | return // nothing to do here 5 | } 6 | const windowFrame = 85 // probably OS and WM dependent 7 | window.outerWidth = window.innerWidth 8 | window.outerHeight = window.innerHeight + windowFrame 9 | } catch (err) { 10 | } 11 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/WebDriver.js: -------------------------------------------------------------------------------- 1 | () => { 2 | if (navigator.webdriver === false) { 3 | // Post Chrome 89.0.4339.0 and already good 4 | } else if (navigator.webdriver === undefined) { 5 | // Pre Chrome 89.0.4339.0 and already good 6 | } else { 7 | // Pre Chrome 88.0.4291.0 and needs patching 8 | delete Object.getPrototypeOf(navigator).webdriver 9 | } 10 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/EnterCaptchaSolutionsResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 4 | 5 | public class EnterCaptchaSolutionsResult 6 | { 7 | public ICollection Solved { get; set; } = new List(); 8 | public ICollection Filtered { get; set; } = new List(); 9 | public string Error { get; set; } 10 | } -------------------------------------------------------------------------------- /Tests/Utils/JsonElementExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace Extra.Tests.Utils; 4 | 5 | public static class JsonElementExtensions 6 | { 7 | public static string GetString(this JsonElement element, string key) 8 | { 9 | return element.GetProperty(key).GetString(); 10 | } 11 | 12 | public static bool GetBoolean(this JsonElement element, string key) 13 | { 14 | return element.GetProperty(key).GetBoolean(); 15 | } 16 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/Codec.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class Codec() : PuppeteerExtraPlugin("stealth-codec") 7 | { 8 | protected internal override Task OnPageCreatedAsync(IPage page) 9 | { 10 | var script = StealthUtils.GetScript("Codec.js"); 11 | return StealthUtils.EvaluateOnNewPage(page, script); 12 | } 13 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/ChromeSci.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class ChromeSci() : PuppeteerExtraPlugin("stealth_sci") 7 | { 8 | protected internal override Task OnPageCreatedAsync(IPage page) 9 | { 10 | var script = StealthUtils.GetScript("SCI.js"); 11 | return StealthUtils.EvaluateOnNewPage(page, script); 12 | } 13 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/LoadTimes.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class LoadTimes() : PuppeteerExtraPlugin("stealth-loadTimes") 7 | { 8 | protected internal override Task OnPageCreatedAsync(IPage page) 9 | { 10 | var script = StealthUtils.GetScript("LoadTimes.js"); 11 | return StealthUtils.EvaluateOnNewPage(page, script); 12 | } 13 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/ChromeRuntime.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class ChromeRuntime() : PuppeteerExtraPlugin("stealth-runtime") 7 | { 8 | protected internal override Task OnPageCreatedAsync(IPage page) 9 | { 10 | var script = StealthUtils.GetScript("Runtime.js"); 11 | return StealthUtils.EvaluateOnNewPage(page, script); 12 | } 13 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/Permissions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class Permissions() : PuppeteerExtraPlugin("stealth-permissions") 7 | { 8 | protected internal override Task OnPageCreatedAsync(IPage page) 9 | { 10 | var script = StealthUtils.GetScript("Permissions.js"); 11 | return StealthUtils.EvaluateOnNewPage(page, script); 12 | } 13 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/CaptchaSolved.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 4 | 5 | public class CaptchaSolved 6 | { 7 | public string Vendor { get; set; } 8 | public string Id { get; set; } 9 | public bool? ResponseElement { get; set; } 10 | public bool? ResponseCallback { get; set; } 11 | public DateTime? SolvedAt { get; set; } 12 | public string Error { get; set; } 13 | public bool? IsSolved { get; set; } 14 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/PluginEvasion.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class PluginEvasion() : PuppeteerExtraPlugin("stealth-pluginEvasion") 7 | { 8 | protected internal override async Task OnPageCreatedAsync(IPage page) 9 | { 10 | var scipt = StealthUtils.GetScript("Plugin.js"); 11 | await StealthUtils.EvaluateOnNewPage(page, scipt); 12 | } 13 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/StackTrace.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class StackTrace() : PuppeteerExtraPlugin("stealth-stackTrace") 7 | { 8 | protected internal override async Task OnPageCreatedAsync(IPage page) 9 | { 10 | var script = StealthUtils.GetScript("Stacktrace.js"); 11 | await page.EvaluateFunctionOnNewDocumentAsync(script); 12 | } 13 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/EnterCaptchaSolutionsResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 3 | 4 | public class EnterCaptchaSolutionsResult 5 | { 6 | public ICollection Solved { get; set; } = new List(); 7 | public ICollection Filtered { get; set; } = new List(); 8 | public string Error { get; set; } 9 | public bool NeedsReload { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/OutDimensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class OutDimensions() : PuppeteerExtraPlugin("stealth-dimensions") 7 | { 8 | protected internal override async Task OnPageCreatedAsync(IPage page) 9 | { 10 | var script = StealthUtils.GetScript("Outdimensions.js"); 11 | await page.EvaluateFunctionOnNewDocumentAsync(script); 12 | } 13 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Utils/QueryHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | 6 | namespace PuppeteerExtraSharp.Utils; 7 | 8 | public static class QueryHelper 9 | { 10 | public static string ToQuery(Dictionary query) 11 | { 12 | if (query == null || query.Count == 0) 13 | return string.Empty; 14 | 15 | return string.Join("&", query.Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}")); 16 | } 17 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/CaptchaSolved.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 5 | 6 | public class CaptchaSolved 7 | { 8 | public CaptchaVendor Vendor { get; set; } 9 | public string Id { get; set; } 10 | public bool? ResponseElement { get; set; } 11 | public bool? ResponseCallback { get; set; } 12 | public DateTime? SolvedAt { get; set; } 13 | public string Error { get; set; } 14 | public bool? IsSolved { get; set; } 15 | } -------------------------------------------------------------------------------- /Tests/ExtraLaunchTest.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Xunit; 3 | 4 | namespace Extra.Tests 5 | { 6 | public class ExtraLaunchTest: BrowserDefault 7 | { 8 | [Fact] 9 | public async void ShouldReturnOkPage() 10 | { 11 | var browser = await this.LaunchAsync(); 12 | var page = await browser.NewPageAsync(); 13 | var response = await page.GoToAsync("http://google.com"); 14 | Assert.Equal(HttpStatusCode.OK, response.Status); 15 | await browser.CloseAsync(); 16 | } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/ContentWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 6 | 7 | public class ContentWindow() : PuppeteerExtraPlugin("Iframe.ContentWindow") 8 | { 9 | public override List Requirements { get; } = [PluginRequirements.RunLast]; 10 | 11 | protected internal override Task OnPageCreatedAsync(IPage page) 12 | { 13 | var script = StealthUtils.GetScript("ContentWindow.js"); 14 | return StealthUtils.EvaluateOnNewPage(page, script); 15 | } 16 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Utils/ResoursesReader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | 4 | namespace PuppeteerExtraSharp.Utils; 5 | 6 | public static class ResourcesReader 7 | { 8 | public static string ReadFile(string path, Assembly customAssembly = null) 9 | { 10 | var assembly = customAssembly ?? Assembly.GetExecutingAssembly(); 11 | using var stream = assembly.GetManifestResourceStream(path); 12 | if (stream is null) 13 | throw new FileNotFoundException($"File with path {path} not found!"); 14 | using var reader = new StreamReader(stream); 15 | return reader.ReadToEnd(); 16 | } 17 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/GetRecaptchaSolutionRequest.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 2 | 3 | public class GetRecaptchaSolutionRequest 4 | { 5 | public string SiteKey { get; set; } 6 | public string PageUrl { get; set; } 7 | public CaptchaVersion Version { get; set; } 8 | 9 | public string DataS; 10 | public string Action { get; set; } 11 | public bool? IsEnterprise { get; set; } 12 | public bool IsInvisible { get; set; } 13 | public double MinV3RecaptchaScore { get; set; } 14 | } 15 | 16 | public enum CaptchaVersion 17 | { 18 | V2, 19 | V3, 20 | // TODO: not supported yet 21 | HCaptcha 22 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Vendors/GeeTest/GeeTestInterceptorScript.js: -------------------------------------------------------------------------------- 1 | window.__geetestCaptured = false; 2 | window.__geetestInstance = null; 3 | 4 | // Intercepter initGeetest4 s'il est défini plus tard 5 | let _initGeetest4 = null; 6 | Object.defineProperty(window, 'initGeetest4', { 7 | get: function() { 8 | return _initGeetest4; 9 | }, 10 | set: function(value) { 11 | _initGeetest4 = function(config, callback) { 12 | return value(config, function(captchaObj) { 13 | window.__geetestInstance = captchaObj; 14 | if (callback) callback(captchaObj); 15 | }); 16 | }; 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/VendorTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 3 | using Xunit; 4 | 5 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 6 | { 7 | public class VendorTest: BrowserDefault 8 | { 9 | [Fact] 10 | public async Task ShouldWork() 11 | { 12 | var plugin = new StealthPlugin(); 13 | var page = await LaunchAndGetPageAsync(plugin); 14 | await page.GoToAsync("https://google.com"); 15 | 16 | var vendor = await page.EvaluateExpressionAsync("navigator.vendor"); 17 | Assert.Equal("Google Inc.", vendor); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/Rules/AggregateCondition.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.BlockResources.Rules; 6 | 7 | internal class AggregateCondition : ICondition 8 | { 9 | private readonly IList _conditions = new List(); 10 | 11 | public AggregateCondition() 12 | { 13 | } 14 | 15 | public AggregateCondition(IList conditions) 16 | { 17 | _conditions = conditions; 18 | } 19 | 20 | public void Add(ICondition condition) 21 | { 22 | _conditions.Add(condition); 23 | } 24 | 25 | public bool IsApplies(IRequest request) 26 | { 27 | return _conditions.All(c => c.IsApplies(request)); 28 | } 29 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/Vendor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class Vendor(StealthVendorSettings settings = null) : PuppeteerExtraPlugin("stealth-vendor") 7 | { 8 | private readonly StealthVendorSettings _settings = settings ?? new StealthVendorSettings("Google Inc."); 9 | 10 | protected internal override async Task OnPageCreatedAsync(IPage page) 11 | { 12 | var script = StealthUtils.GetScript("Vendor.js"); 13 | await page.EvaluateFunctionOnNewDocumentAsync(script, _settings.Vendor); 14 | } 15 | } 16 | 17 | public class StealthVendorSettings(string vendor) : IPuppeteerExtraPluginOptions 18 | { 19 | public string Vendor { get; } = vendor; 20 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/StealthPluginTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 3 | using PuppeteerSharp; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 7 | { 8 | public class StealthPluginTest: BrowserDefault 9 | { 10 | [Fact] 11 | public async Task Test() 12 | { 13 | var browser = await LaunchWithPluginAsync(new StealthPlugin()); 14 | var page = await browser.NewPageAsync(); 15 | await page.GoToAsync("https://bot.sannysoft.com"); 16 | await page.ScreenshotAsync("Stealth.png", new ScreenshotOptions() 17 | { 18 | FullPage = true, 19 | Type = ScreenshotType.Png, 20 | }); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/UserAgentTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Extra.Tests.Utils; 3 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 7 | { 8 | public class UserAgentTest: BrowserDefault 9 | { 10 | [Fact] 11 | public async Task ShouldWork() 12 | { 13 | var plugin = new StealthPlugin(); 14 | var page = await LaunchAndGetPageAsync(plugin); 15 | await page.GoToAsync("https://google.com"); 16 | var userAgent = await page.Browser.GetUserAgentAsync(); 17 | 18 | var finger = await new FingerPrint().GetFingerPrint(page); 19 | Assert.DoesNotContain("HeadlessChrome", finger.GetString("userAgent")); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Providers/CapSolver/Models/CapSolverCreateTaskResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers.CapSolver.Models; 3 | 4 | internal class CapSolverCreateTaskResponse 5 | { 6 | public int ErrorId { get; set; } 7 | public string TaskId { get; set; } 8 | } 9 | 10 | internal class CapSolverGetTaskResult 11 | { 12 | public int ErrorId { get; set; } 13 | public string ErrorCode { get; set; } 14 | public string ErrorDescription { get; set; } 15 | public string Status { get; set; } 16 | public JsonElement Solution { get; set; } 17 | public string Cost { get; set; } 18 | public string Ip { get; set; } 19 | public long CreateTime { get; set; } 20 | public long EndTime { get; set; } 21 | public int SolveCount { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Providers/TwoCaptcha/Models/TwoCaptchaCreateTaskResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers.TwoCaptcha.Models; 3 | 4 | internal class TwoCaptchaCreateTaskResponse 5 | { 6 | public int ErrorId { get; set; } 7 | public ulong TaskId { get; set; } 8 | } 9 | 10 | internal class TwoCaptchaGetTaskResult 11 | { 12 | public int ErrorId { get; set; } 13 | public string ErrorCode { get; set; } 14 | public string ErrorDescription { get; set; } 15 | public string Status { get; set; } 16 | public JsonElement Solution { get; set; } 17 | public string Cost { get; set; } 18 | public string Ip { get; set; } 19 | public long CreateTime { get; set; } 20 | public long EndTime { get; set; } 21 | public int SolveCount { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/PluginTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Utils; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 8 | { 9 | public class PluginTest : BrowserDefault 10 | { 11 | 12 | public async Task HasMimeTypes() 13 | { 14 | var plugin = new StealthPlugin(); 15 | var page = await LaunchAndGetPageAsync(plugin); 16 | await page.GoToAsync("https://google.com"); 17 | 18 | var finger = await new FingerPrint().GetFingerPrint(page); 19 | 20 | Assert.Equal(3, finger.GetProperty("plugins").EnumerateArray().Count()); 21 | Assert.Equal(4, finger.GetProperty("mimeTypes").EnumerateArray().Count()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/Languages.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 6 | 7 | public class Languages(StealthLanguagesOptions options = null) : PuppeteerExtraPlugin("stealth-language") 8 | { 9 | public StealthLanguagesOptions Options { get; } = options ?? new StealthLanguagesOptions("en-US", "en"); 10 | 11 | protected internal override Task OnPageCreatedAsync(IPage page) 12 | { 13 | var script = StealthUtils.GetScript("Language.js"); 14 | return StealthUtils.EvaluateOnNewPage(page, script, Options.Languages); 15 | } 16 | } 17 | 18 | public class StealthLanguagesOptions(params string[] languages) : IPuppeteerExtraPluginOptions 19 | { 20 | public object[] Languages { get; } = languages.Cast().ToArray(); 21 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/HardwareConcurrency.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class HardwareConcurrency(StealthHardwareConcurrencyOptions options = null) 7 | : PuppeteerExtraPlugin("stealth/hardwareConcurrency") 8 | { 9 | public StealthHardwareConcurrencyOptions Options { get; } = options ?? new StealthHardwareConcurrencyOptions(4); 10 | 11 | protected internal override Task OnPageCreatedAsync(IPage page) 12 | { 13 | var script = StealthUtils.GetScript("HardwareConcurrency.js"); 14 | return StealthUtils.EvaluateOnNewPage(page, script, Options.Concurrency); 15 | } 16 | } 17 | 18 | public class StealthHardwareConcurrencyOptions(int concurrency) : IPuppeteerExtraPluginOptions 19 | { 20 | public int Concurrency { get; } = concurrency; 21 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/WebGl.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class WebGl(StealthWebGLOptions options) : PuppeteerExtraPlugin("stealth-webGl") 7 | { 8 | private readonly StealthWebGLOptions _options = 9 | options ?? new StealthWebGLOptions("Intel Inc.", "Intel Iris OpenGL Engine"); 10 | 11 | protected internal override async Task OnPageCreatedAsync(IPage page) 12 | { 13 | var script = StealthUtils.GetScript("WebGL.js"); 14 | await page.EvaluateFunctionOnNewDocumentAsync(script, _options.Vendor, _options.Renderer); 15 | } 16 | } 17 | 18 | public class StealthWebGLOptions(string vendor, string renderer) : IPuppeteerExtraPluginOptions 19 | { 20 | public string Vendor { get; } = vendor; 21 | public string Renderer { get; } = renderer; 22 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/PluginEvasionTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Utils; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 8 | { 9 | public class PluginEvasionTest : BrowserDefault 10 | { 11 | public async Task ShouldNotHaveModifications() 12 | { 13 | var stealthPlugin = new StealthPlugin(); 14 | var page = await LaunchAndGetPageAsync(stealthPlugin); 15 | 16 | await page.GoToAsync("https://google.com"); 17 | 18 | 19 | var fingerPrint = await new FingerPrint().GetFingerPrint(page); 20 | 21 | Assert.Equal(3, fingerPrint.GetProperty("plugins").EnumerateArray().Count()); 22 | Assert.Equal(4, fingerPrint.GetProperty("mimeTypes").EnumerateArray().Count()); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Interfaces/ICaptchaSolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 5 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 6 | using PuppeteerSharp; 7 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 8 | 9 | public interface ICaptchaSolver 10 | { 11 | public Task> WaitForCaptchasAsync(IPage page, TimeSpan timeout); 12 | public Task> FindCaptchasAsync(HashSet vendors, IPage page); 13 | public Task> SolveCaptchasAsync(IPage page, ICollection captchas); 14 | public Task> EnterCaptchaSolutionsAsync(IPage page, ICollection solutions); 15 | public Task OnPageCreatedAsync(IPage page); 16 | } 17 | -------------------------------------------------------------------------------- /Tests/Utils/FingerPrint.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Utils; 5 | using PuppeteerSharp; 6 | 7 | namespace Extra.Tests.Utils 8 | { 9 | public class FingerPrint 10 | { 11 | /// 12 | /// https://antoinevastel.com/bots/ 13 | /// 14 | /// 15 | /// 16 | public async Task GetFingerPrint(IPage page) 17 | { 18 | var script = ResourcesReader.ReadFile("Extra.Tests.StealthPluginTests.Script.fpCollect.js", Assembly.GetExecutingAssembly()); 19 | await page.EvaluateExpressionAsync(script); 20 | 21 | var fingerPrint = 22 | await page.EvaluateFunctionAsync("async () => await fpCollect().generateFingerprint()"); 23 | 24 | return fingerPrint; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/CapSolver/Models/CapSolverCreateTaskResponse.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.CapSolver.Models; 2 | 3 | internal class CapSolverCreateTaskResponse 4 | { 5 | public int ErrorId { get; set; } 6 | public string TaskId { get; set; } 7 | } 8 | 9 | internal class CapSolverGetTaskResult 10 | { 11 | public int ErrorId { get; set; } 12 | public string ErrorCode { get; set; } 13 | public string ErrorDescription { get; set; } 14 | public string Status { get; set; } 15 | public CapSolverSolution Solution { get; set; } 16 | public string Cost { get; set; } 17 | public string Ip { get; set; } 18 | public long CreateTime { get; set; } 19 | public long EndTime { get; set; } 20 | public int SolveCount { get; set; } 21 | } 22 | 23 | internal class CapSolverSolution 24 | { 25 | public string GRecaptchaResponse { get; set; } 26 | public string Token { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/TwoCaptcha/Models/TwoCaptchaCreateTaskResponse.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.TwoCaptcha.Models; 2 | 3 | internal class TwoCaptchaCreateTaskResponse 4 | { 5 | public int ErrorId { get; set; } 6 | public ulong TaskId { get; set; } 7 | } 8 | 9 | internal class TwoCaptchaGetTaskResult 10 | { 11 | public int ErrorId { get; set; } 12 | public string ErrorCode { get; set; } 13 | public string ErrorDescription { get; set; } 14 | public string Status { get; set; } 15 | public TwoCaptchaSolution Solution { get; set; } 16 | public string Cost { get; set; } 17 | public string Ip { get; set; } 18 | public long CreateTime { get; set; } 19 | public long EndTime { get; set; } 20 | public int SolveCount { get; set; } 21 | } 22 | 23 | internal class TwoCaptchaSolution 24 | { 25 | public string GRecaptchaResponse { get; set; } 26 | public string Token { get; set; } 27 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Interfaces/ICaptchaVendor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 5 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 6 | using PuppeteerSharp; 7 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 8 | 9 | public interface ICaptchaVendor 10 | { 11 | CaptchaVendor Vendor { get; } 12 | public Task WaitForCaptchasAsync(IPage page, TimeSpan timeout); 13 | public Task FindCaptchasAsync(IPage page); 14 | public Task> SolveCaptchasAsync(IPage page, ICollection captchas); 15 | public Task EnterCaptchaSolutionsAsync(IPage page, ICollection solutions); 16 | public Task HandleOnPageCreatedAsync(IPage page); 17 | void ProcessResponseAsync(IPage page, object? send, ResponseCreatedEventArgs e); 18 | } 19 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/Captcha.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 2 | 3 | public class Captcha 4 | { 5 | public string Sitekey { get; set; } 6 | public string Callback { get; set; } 7 | public string Vendor { get; set; } 8 | public string Id { get; set; } 9 | public string S { get; set; } 10 | public int WidgetId { get; set; } 11 | public CaptchaDisplay Display { get; set; } 12 | public string Action { get; set; } 13 | public string Url { get; set; } 14 | public bool HasResponseElement { get; set; } 15 | public bool IsEnterprise { get; set; } 16 | public bool IsInViewport { get; set; } 17 | public bool IsInvisible { get; set; } 18 | public bool HasActiveChallengePopup { get; set; } 19 | public bool HasChallengeFrame { get; set; } 20 | public CaptchaType CaptchaType { get; set; } 21 | 22 | public Captcha() 23 | { 24 | Display = new CaptchaDisplay(); 25 | } 26 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/AnonymizeUa/AnonymizeUaPlugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using System.Threading.Tasks; 4 | using PuppeteerSharp; 5 | 6 | namespace PuppeteerExtraSharp.Plugins.AnonymizeUa; 7 | 8 | public class AnonymizeUaPlugin() : PuppeteerExtraPlugin("anonymize-ua") 9 | { 10 | private Func _customAction; 11 | 12 | public void CustomizeUa(Func uaAction) 13 | { 14 | _customAction = uaAction; 15 | } 16 | 17 | protected internal override async Task OnPageCreatedAsync(IPage page) 18 | { 19 | var ua = await page.Browser.GetUserAgentAsync(); 20 | ua = ua.Replace("HeadlessChrome", "Chrome"); 21 | 22 | var regex = new Regex(@"/\(([^)]+)\)/"); 23 | ua = regex.Replace(ua, "(Windows NT 10.0; Win64; x64)"); 24 | 25 | if (_customAction != null) 26 | ua = _customAction(ua); 27 | 28 | await page.SetUserAgentAsync(ua); 29 | } 30 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/ICaptchaHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver; 5 | using PuppeteerExtraSharp.Plugins.Recaptcha.Models; 6 | using PuppeteerSharp; 7 | 8 | namespace PuppeteerExtraSharp.Plugins.Recaptcha; 9 | 10 | [Obsolete($"Use {nameof(CaptchaSolverPlugin)} instead. This plugin will be removed in a future version.", UrlFormat = "https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/CaptchaSolver")] 11 | public interface ICaptchaHandler 12 | { 13 | public Task WaitForCaptchasAsync(IPage page, TimeSpan timeout); 14 | public Task FindCaptchasAsync(IPage page); 15 | public Task> SolveCaptchasAsync(IPage page, ICollection captchas); 16 | public Task EnterCaptchaSolutionsAsync(IPage page, ICollection solutions); 17 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/SourceUrl/fixtures/Test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IPage Title 6 | 7 | 8 | 9 |

Please use `document.querySelector`..

10 | 11 | 35 | 36 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/CapSolver/CapSolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.CapSolver; 4 | 5 | public class CapSolver : IRecaptchaProvider 6 | { 7 | private readonly CaptchaProviderOptions _options; 8 | private readonly CapSolverApi _api; 9 | 10 | public CapSolver(string key, CaptchaProviderOptions options = null) 11 | { 12 | _options = options ?? new CaptchaProviderOptions(); 13 | _api = new CapSolverApi(key, _options); 14 | } 15 | 16 | public async Task GetSolutionAsync(GetRecaptchaSolutionRequest request) 17 | { 18 | var task = await _api.CreateTaskAsync(request); 19 | 20 | await Task.Delay(_options.StartTimeout); 21 | 22 | var result = await _api.GetSolution(task.TaskId); 23 | 24 | if (result.Solution == null) 25 | { 26 | throw new ArgumentNullException(nameof(result.Solution), "Captcha solution can't be null"); 27 | } 28 | 29 | return result.Solution.GRecaptchaResponse; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/TwoCaptcha/TwoCaptcha.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.TwoCaptcha; 5 | 6 | public class TwoCaptcha : IRecaptchaProvider 7 | { 8 | private readonly CaptchaProviderOptions _options; 9 | private readonly TwoCaptchaApi _api; 10 | 11 | public TwoCaptcha(string key, CaptchaProviderOptions options = null) 12 | { 13 | _options = options ?? new CaptchaProviderOptions(); 14 | _api = new TwoCaptchaApi(key, _options); 15 | } 16 | 17 | public async Task GetSolutionAsync(GetRecaptchaSolutionRequest request) 18 | { 19 | var task = await _api.CreateTaskAsync(request); 20 | 21 | await Task.Delay(_options.StartTimeout); 22 | 23 | var result = await _api.GetSolution(task.TaskId); 24 | 25 | if (result.Solution == null) 26 | { 27 | throw new ArgumentNullException(nameof(result.Solution), "Captcha solution can't be null"); 28 | } 29 | 30 | return result.Solution.GRecaptchaResponse; 31 | } 32 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/WebDriver.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 6 | 7 | public class WebDriver() : PuppeteerExtraPlugin("stealth-webDriver") 8 | { 9 | protected internal override async Task OnPageCreatedAsync(IPage page) 10 | { 11 | var script = StealthUtils.GetScript("WebDriver.js"); 12 | await page.EvaluateFunctionOnNewDocumentAsync(script); 13 | } 14 | 15 | protected internal override Task BeforeLaunchAsync(LaunchOptions options) 16 | { 17 | var args = options.Args.ToList(); 18 | var idx = args.FindIndex(e => e.StartsWith("--disable-blink-features=")); 19 | if (idx != -1) 20 | { 21 | var arg = args[idx]; 22 | args[idx] = $"{arg}, AutomationControlled"; 23 | return Task.CompletedTask; 24 | } 25 | 26 | args.Add("--disable-blink-features=AutomationControlled"); 27 | 28 | options.Args = args.ToArray(); 29 | 30 | return Task.CompletedTask; 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Overmiind 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/WebDriverTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 3 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 7 | { 8 | public class WebDriverTest : BrowserDefault 9 | { 10 | [Fact] 11 | public async Task ShouldWork() 12 | { 13 | var plugin = new StealthPlugin(); 14 | var page = await LaunchAndGetPageAsync(plugin); 15 | await page.GoToAsync("https://google.com"); 16 | 17 | var driver = await page.EvaluateExpressionAsync("navigator.webdriver"); 18 | Assert.False(driver); 19 | } 20 | 21 | [Fact] 22 | public async Task WontKillOtherMethods() 23 | { 24 | var plugin = new WebDriver(); 25 | var page = await LaunchAndGetPageAsync(plugin); 26 | await page.GoToAsync("https://google.com"); 27 | 28 | var data = await page.EvaluateExpressionAsync("navigator.javaEnabled()"); 29 | Assert.False(data); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Providers/CapSolver/CapSolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 5 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers.CapSolver; 6 | 7 | public class CapSolver : ICaptchaSolverProvider 8 | { 9 | private readonly CaptchaProviderOptions _options; 10 | private readonly CapSolverApi _api; 11 | 12 | public CapSolver(string key, CaptchaProviderOptions options = null) 13 | { 14 | _options = options ?? new CaptchaProviderOptions(); 15 | _api = new CapSolverApi(key, _options); 16 | } 17 | 18 | public async Task GetSolutionAsync(GetCaptchaSolutionRequest request) 19 | { 20 | var task = await _api.CreateTaskAsync(request); 21 | 22 | await Task.Delay(_options.StartTimeout); 23 | 24 | var result = await _api.GetSolution(task.TaskId); 25 | 26 | if (result.Solution.ValueKind is JsonValueKind.Undefined) 27 | { 28 | throw new ArgumentNullException(nameof(result.Solution), "Captcha solution can't be null"); 29 | } 30 | 31 | return result.Solution.ToString(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Providers/TwoCaptcha/TwoCaptcha.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 5 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers.TwoCaptcha; 6 | 7 | public class TwoCaptcha : ICaptchaSolverProvider 8 | { 9 | private readonly CaptchaProviderOptions _options; 10 | private readonly TwoCaptchaApi _api; 11 | 12 | public TwoCaptcha(string key, CaptchaProviderOptions options = null) 13 | { 14 | _options = options ?? new CaptchaProviderOptions(); 15 | _api = new TwoCaptchaApi(key, _options); 16 | } 17 | 18 | public async Task GetSolutionAsync(GetCaptchaSolutionRequest request) 19 | { 20 | var task = await _api.CreateTaskAsync(request); 21 | 22 | await Task.Delay(_options.StartTimeout); 23 | 24 | var result = await _api.GetSolution(task.TaskId); 25 | 26 | if (result.Solution.ValueKind is JsonValueKind.Undefined) 27 | { 28 | throw new ArgumentNullException(nameof(result.Solution), "Captcha solution can't be null"); 29 | } 30 | 31 | return result.Solution.ToString(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/SCI.js: -------------------------------------------------------------------------------- 1 | () => { 2 | if (!window.chrome) { 3 | // Use the exact property descriptor found in headful Chrome 4 | // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')` 5 | Object.defineProperty(window, 'chrome', { 6 | writable: true, 7 | enumerable: true, 8 | configurable: false, // note! 9 | value: {} // We'll extend that later 10 | }) 11 | } 12 | 13 | // That means we're running headful and don't need to mock anything 14 | if ('csi' in window.chrome) { 15 | return // Nothing to do here 16 | } 17 | 18 | // Check that the Navigation Timing API v1 is available, we need that 19 | if (!window.performance || !window.performance.timing) { 20 | return 21 | } 22 | 23 | const {timing} = window.performance 24 | 25 | window.chrome.csi = function () { 26 | return { 27 | onloadT: timing.domContentLoadedEventEnd, 28 | startE: timing.navigationStart, 29 | pageT: Date.now() - timing.navigationStart, 30 | tran: 15 // Transition type or something 31 | } 32 | } 33 | utils.patchToString(window.chrome.csi) 34 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/Rules/ResourcesBlockBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PuppeteerSharp; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.BlockResources.Rules; 5 | 6 | public class ResourcesBlockBuilder 7 | { 8 | private readonly AggregateCondition _aggregateCondition = new(); 9 | 10 | internal ResourcesBlockBuilder() 11 | { 12 | } 13 | 14 | public ResourcesBlockBuilder Resources(params ResourceType[] resources) 15 | { 16 | _aggregateCondition.Add(new ResourceCondition(resources)); 17 | return this; 18 | } 19 | 20 | public ResourcesBlockBuilder Page(IPage page) 21 | { 22 | _aggregateCondition.Add(new PageCondition(page)); 23 | return this; 24 | } 25 | 26 | public ResourcesBlockBuilder Url(string pattern) 27 | { 28 | _aggregateCondition.Add(new UrlPatternCondition(pattern)); 29 | return this; 30 | } 31 | 32 | public ResourcesBlockBuilder Custom(Func customCondition) 33 | { 34 | _aggregateCondition.Add(new PredicateCondition(customCondition)); 35 | return this; 36 | } 37 | 38 | internal IBlockRule Build() 39 | { 40 | return new PredicateBlockRule(e => _aggregateCondition.IsApplies(e)); 41 | } 42 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/StealthPluginTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 3 | using Xunit; 4 | 5 | namespace Extra.Tests.StealthPluginTests 6 | { 7 | public class StealthPluginTests: BrowserDefault 8 | { 9 | [Fact] 10 | public async Task ShouldBeNotDetected() 11 | { 12 | var plugin = new StealthPlugin(); 13 | var page = await LaunchAndGetPageAsync(plugin); 14 | await page.GoToAsync("https://google.com"); 15 | 16 | var webdriver = await page.EvaluateExpressionAsync("navigator.webdriver"); 17 | Assert.False(webdriver); 18 | 19 | var headlessUserAgent = await page.EvaluateExpressionAsync("window.navigator.userAgent"); 20 | Assert.DoesNotContain("Headless", headlessUserAgent); 21 | 22 | var webDriverOverriden = 23 | await page.EvaluateExpressionAsync( 24 | "Object.getOwnPropertyDescriptor(navigator.__proto__, 'webdriver') !== undefined"); 25 | Assert.True(webDriverOverriden); 26 | 27 | var plugins = await page.EvaluateExpressionAsync("navigator.plugins.length"); 28 | Assert.NotEqual(0, plugins); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/CaptchaSolverTests/CaptchaSolverTestsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Extra.Tests.Properties; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 5 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers; 6 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers.CapSolver; 7 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers.TwoCaptcha; 8 | 9 | namespace Extra.Tests.CaptchaSolverTests; 10 | 11 | public abstract class CaptchaSolverTestsBase : BrowserDefault 12 | { 13 | protected static CaptchaProviderOptions Options = new() 14 | { 15 | StartTimeout = TimeSpan.FromSeconds(10), 16 | MaxPollingAttempts = 30, 17 | ApiTimeout = TimeSpan.FromMinutes(3), 18 | }; 19 | 20 | public static IEnumerable Providers => GetDefaultProviders(); 21 | 22 | private static IEnumerable GetDefaultProviders() 23 | { 24 | return new List() 25 | { 26 | new ICaptchaSolverProvider[] 27 | { 28 | new TwoCaptcha(Resources.TwoCaptchaKey, Options), 29 | }, 30 | new ICaptchaSolverProvider[] 31 | { 32 | new CapSolver(Resources.CapSolverKey, Options), 33 | } 34 | }; 35 | } 36 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/StealthUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Utils; 5 | using PuppeteerSharp; 6 | 7 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth; 8 | 9 | internal static class StealthUtils 10 | { 11 | private static readonly ConcurrentDictionary _scriptCache = new(); 12 | private static readonly string _scriptsNamespace = $"{typeof(StealthUtils).Namespace}.Scripts"; 13 | 14 | public static Task EvaluateOnNewPage(IPage page, string script, params object[] args) 15 | { 16 | ArgumentNullException.ThrowIfNull(page); 17 | ArgumentException.ThrowIfNullOrWhiteSpace(script); 18 | 19 | return page.IsClosed 20 | ? Task.CompletedTask 21 | : page.EvaluateFunctionOnNewDocumentAsync(script, args); 22 | } 23 | 24 | public static string GetScript(string name) 25 | { 26 | ArgumentException.ThrowIfNullOrWhiteSpace(name); 27 | 28 | return _scriptCache.GetOrAdd(name, static n => 29 | { 30 | var resourceName = BuildResourceName(n); 31 | return ResourcesReader.ReadFile(resourceName); 32 | }); 33 | } 34 | 35 | private static string BuildResourceName(string name) => $"{_scriptsNamespace}.{name}"; 36 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/WebGL.js: -------------------------------------------------------------------------------- 1 | (vendor, renderer) => { 2 | const getParameterProxyHandler = { 3 | apply: function (target, ctx, args) { 4 | const param = (args || [])[0]; 5 | // UNMASKED_VENDOR_WEBGL 6 | if (param === 37445) { 7 | return vendor || 'Intel Inc.'; // default in headless: Google Inc. 8 | } 9 | // UNMASKED_RENDERER_WEBGL 10 | if (param === 37446) { 11 | return renderer || 'Intel Iris OpenGL Engine'; // default in headless: Google SwiftShader 12 | } 13 | return utils.cache.Reflect.apply(target, ctx, args); 14 | } 15 | }; 16 | 17 | // There's more than one WebGL rendering context 18 | // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext#Browser_compatibility 19 | // To find out the original values here: Object.getOwnPropertyDescriptors(WebGLRenderingContext.prototype.getParameter) 20 | const addProxy = (obj, propName) => { 21 | utils.replaceWithProxy(obj, propName, getParameterProxyHandler); 22 | }; 23 | // For whatever weird reason loops don't play nice with Object.defineProperty, here's the next best thing: 24 | addProxy(WebGLRenderingContext.prototype, 'getParameter'); 25 | addProxy(WebGL2RenderingContext.prototype, 'getParameter'); 26 | } -------------------------------------------------------------------------------- /Tests/CaptchaSolverTests/CloudflareTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.CaptchaSolver; 3 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.CaptchaSolverTests; 8 | 9 | public class CloudflareTests : CaptchaSolverTestsBase 10 | { 11 | [Theory] 12 | [MemberData(nameof(Providers))] 13 | public async Task ShouldSolveCheckbox(ICaptchaSolverProvider provider) 14 | { 15 | var plugin = new CaptchaSolverPlugin(provider); 16 | var page = await LaunchAndGetPageAsync(plugin); 17 | 18 | await page.GoToAsync("https://clifford.io/demo/cloudflare-turnstile"); 19 | 20 | var result = await plugin.SolveCaptchaAsync(page, new CaptchaOptions 21 | { 22 | SolveInViewportOnly = true, 23 | SolveScoreBased = false, 24 | }); 25 | 26 | Assert.Null(result.Error); 27 | Assert.NotEmpty(result.Solved); 28 | Assert.Empty(result.Filtered); 29 | 30 | await page.ClickAsync("button[type='submit']"); 31 | await Task.Delay(2000); 32 | var answerElement = 33 | await page.EvaluateExpressionAsync("document.querySelector(\"body\").textContent"); 34 | 35 | Assert.Contains("Passed Cloudflare Turnstile check", answerElement); 36 | } 37 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/CaptchaOptionsScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver; 6 | 7 | public class CaptchaOptionsScope(CaptchaOptions defaultOptions) 8 | { 9 | private readonly AsyncLocal _currentScope = new(); 10 | private readonly CaptchaOptions _defaultOptions = defaultOptions ?? new CaptchaOptions(); 11 | 12 | public CaptchaOptions Current => _currentScope.Value ?? _defaultOptions; 13 | 14 | public IDisposable CreateScope(CaptchaOptions overrides) 15 | { 16 | var parent = _currentScope.Value; 17 | _currentScope.Value = overrides; 18 | return new ScopeDisposer(this, parent); 19 | } 20 | 21 | public IDisposable CreateScope(Func configure) 22 | { 23 | var current = Current; 24 | var newOptions = configure(current); 25 | return CreateScope(newOptions); 26 | } 27 | 28 | private class ScopeDisposer(CaptchaOptionsScope provider, CaptchaOptions parentSettings) 29 | : IDisposable 30 | { 31 | private bool _disposed; 32 | 33 | public void Dispose() 34 | { 35 | if (_disposed) return; 36 | provider._currentScope.Value = parentSettings; 37 | _disposed = true; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/LanguagesTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Utils; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 5 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 6 | using Xunit; 7 | 8 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 9 | { 10 | public class LanguagesTest : BrowserDefault 11 | { 12 | [Fact] 13 | public async Task ShouldWork() 14 | { 15 | var plugin = new Languages(); 16 | var page = await LaunchAndGetPageAsync(plugin); 17 | 18 | await page.GoToAsync("https://google.com"); 19 | 20 | var fingerPrint = await new FingerPrint().GetFingerPrint(page); 21 | 22 | Assert.Contains("en-US", fingerPrint.GetProperty("languages").EnumerateArray().Select(e => e.GetString())); 23 | } 24 | 25 | 26 | [Fact] 27 | public async Task ShouldWorkWithCustomSettings() 28 | { 29 | var stealthPlugin = new StealthPlugin(new StealthLanguagesOptions("fr-FR", "bl-BL")); 30 | 31 | var page = await LaunchAndGetPageAsync(stealthPlugin); 32 | 33 | await page.GoToAsync("https://google.com"); 34 | 35 | var fingerPrint = await new FingerPrint().GetFingerPrint(page); 36 | 37 | Assert.Contains("fr-FR", fingerPrint.GetProperty("languages").EnumerateArray().Select(e => e.GetString())); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Providers/GetCaptchaSolutionRequest.cs: -------------------------------------------------------------------------------- 1 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 2 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers; 3 | 4 | public class GetCaptchaSolutionRequest 5 | { 6 | public string SiteKey { get; set; } 7 | public string PageUrl { get; set; } 8 | public CaptchaVersion Version { get; set; } 9 | public CaptchaVendor Vendor { get; set; } 10 | public string DataS; 11 | public string Action { get; set; } 12 | public bool? IsEnterprise { get; set; } 13 | public bool IsInvisible { get; set; } 14 | public double MinScore { get; set; } 15 | 16 | // GeeTest-specific 17 | public string? Gt { get; set; } 18 | public string? Challenge { get; set; } 19 | public string? CaptchaId { get; set; } 20 | 21 | // DataDome-specific 22 | public string? DataDomeCaptchaUrl { get; set; } 23 | public string? DataDomeCid { get; set; } 24 | public string? DataDomeHash { get; set; } 25 | public string? DataDomeUserAgent { get; set; } 26 | public string? DataDomeReferer { get; set; } 27 | 28 | // Proxy settings (required for DataDome) 29 | public string? ProxyType { get; set; } 30 | public string? ProxyAddress { get; set; } 31 | public int? ProxyPort { get; set; } 32 | public string? ProxyLogin { get; set; } 33 | public string? ProxyPassword { get; set; } 34 | public string? ProxySessionId { get; set; } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/SourceUrl/SourceUrlTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using PuppeteerSharp; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.StealthPluginTests.EvasionsTests.SourceUrl 7 | { 8 | public class SourceUrlTest : BrowserDefault 9 | { 10 | private readonly string _pageUrl = Environment.CurrentDirectory + "\\StealthPluginTests\\EvasionsTests\\SourceUrl\\fixtures\\Test.html"; 11 | 12 | // [Fact] 13 | public async Task ShouldWork() 14 | { 15 | var plugin = new PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions.SourceUrl(); 16 | 17 | var page = await LaunchAndGetPageAsync(plugin); 18 | await page.GoToAsync(_pageUrl, WaitUntilNavigation.Load); 19 | 20 | await page.EvaluateExpressionAsync("document.querySelector('title')"); 21 | var result = await page.EvaluateExpressionAsync("document.querySelector('#result').innerText"); 22 | 23 | var result2 = await page.EvaluateFunctionAsync(@"() => { 24 | try { 25 | Function.prototype.toString.apply({}) 26 | } catch (err) { 27 | return err.stack 28 | }}"); 29 | 30 | Assert.Equal("PASS", result); 31 | Assert.DoesNotContain("__puppeteer_evaluation_script", result2); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/SourceUrl.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Threading.Tasks; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 6 | 7 | public class SourceUrl() : PuppeteerExtraPlugin("SourceUrl") 8 | { 9 | protected internal override Task OnPageCreatedAsync(IPage page) 10 | { 11 | var mainWordProperty = 12 | page.MainFrame.GetType().GetProperty("MainWorld", BindingFlags.NonPublic 13 | | BindingFlags.Public | BindingFlags.Instance); 14 | var mainWordGetters = mainWordProperty.GetGetMethod(true); 15 | 16 | page.Load += async (_, _) => 17 | { 18 | var mainWord = mainWordGetters.Invoke(page.MainFrame, null); 19 | var contextField = mainWord.GetType() 20 | .GetField("_contextResolveTaskWrapper", BindingFlags.NonPublic | BindingFlags.Instance); 21 | if (contextField is not null) 22 | { 23 | var context = (TaskCompletionSource)contextField.GetValue(mainWord); 24 | var execution = await context.Task; 25 | var suffixField = execution.GetType() 26 | .GetField("EvaluationScriptSuffix", BindingFlags.NonPublic | BindingFlags.Instance); 27 | suffixField?.SetValue(execution, "//# sourceURL=''"); 28 | } 29 | }; 30 | 31 | return Task.CompletedTask; 32 | } 33 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/Permissions.js: -------------------------------------------------------------------------------- 1 | () => { 2 | const isSecure = document.location.protocol.startsWith('https'); 3 | 4 | // In headful on secure origins the permission should be "default", not "denied" 5 | if (isSecure) { 6 | utils.replaceGetterWithProxy(Notification, 'permission', { 7 | apply() { 8 | return 'default'; 9 | } 10 | }); 11 | } 12 | 13 | // Another weird behavior: 14 | // On insecure origins in headful the state is "denied", 15 | // whereas in headless it's "prompt" 16 | if (!isSecure) { 17 | const handler = { 18 | apply(target, ctx, args) { 19 | const param = (args || [])[0]; 20 | 21 | const isNotifications = 22 | param && param.name && param.name === 'notifications'; 23 | if (!isNotifications) { 24 | return utils.cache.Reflect.apply(...arguments); 25 | } 26 | 27 | return Promise.resolve( 28 | Object.setPrototypeOf( 29 | { 30 | state: 'denied', 31 | onchange: null 32 | }, 33 | PermissionStatus.prototype 34 | ) 35 | ); 36 | } 37 | }; 38 | // Note: Don't use `Object.getPrototypeOf` here 39 | utils.replaceWithProxy(Permissions.prototype, 'query', handler); 40 | } 41 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/PermissionsTest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Utils; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 8 | { 9 | public class PermissionsTest : BrowserDefault 10 | { 11 | [Fact] 12 | public async Task ShouldBeDenied() 13 | { 14 | var plugin = new StealthPlugin(); 15 | var page = await LaunchAndGetPageAsync(); 16 | 17 | var notificationPermission = await page.EvaluateFunctionAsync(GetNotificationPermissionScript); 18 | Assert.Equal("denied", notificationPermission.GetString("state")); 19 | Assert.Equal("denied", notificationPermission.GetString("permission")); 20 | 21 | await page.GoToAsync("https://example.com"); 22 | 23 | notificationPermission = await page.EvaluateFunctionAsync(GetNotificationPermissionScript); 24 | Assert.Equal("prompt", notificationPermission.GetString("state")); 25 | Assert.Equal("default", notificationPermission.GetString("permission")); 26 | } 27 | 28 | private const string GetNotificationPermissionScript = 29 | @"async () => { 30 | const { state, onchange } = await navigator.permissions.query({ 31 | name: 'notifications' 32 | }) 33 | return { 34 | state, 35 | onchange, 36 | permission: Notification.permission 37 | } 38 | }"; 39 | } 40 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/Stacktrace.js: -------------------------------------------------------------------------------- 1 | () => { 2 | const errors = { 3 | Error, 4 | EvalError, 5 | RangeError, 6 | ReferenceError, 7 | SyntaxError, 8 | TypeError, 9 | URIError 10 | } 11 | for (const name in errors) { 12 | // eslint-disable-next-line 13 | globalThis[name] = (function (NativeError) { 14 | return function (message) { 15 | const err = new NativeError(message) 16 | const stub = { 17 | message: err.message, 18 | name: err.name, 19 | toString: () => err.toString(), 20 | get stack() { 21 | const lines = err.stack.split('\n') 22 | lines.splice(1, 1) // remove anonymous function above 23 | lines.pop() // remove puppeteer line 24 | return lines.join('\n') 25 | } 26 | } 27 | // eslint-disable-next-line 28 | if (this === globalThis) { 29 | // called as function, not constructor 30 | // eslint-disable-next-line 31 | stub.__proto__ = NativeError 32 | return stub 33 | } 34 | Object.assign(this, stub) 35 | // eslint-disable-next-line 36 | this.__proto__ = NativeError 37 | } 38 | })(errors[name]) 39 | } 40 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/Captcha.cs: -------------------------------------------------------------------------------- 1 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 4 | 5 | public class Captcha 6 | { 7 | public string Sitekey { get; set; } 8 | public string Callback { get; set; } 9 | public CaptchaVendor Vendor { get; set; } 10 | public string Id { get; set; } 11 | public string S { get; set; } 12 | public int WidgetId { get; set; } 13 | public CaptchaDisplay Display { get; set; } 14 | public string Action { get; set; } 15 | public string Url { get; set; } 16 | public bool HasResponseElement { get; set; } 17 | public bool IsEnterprise { get; set; } 18 | public bool IsInViewport { get; set; } 19 | public bool IsInvisible { get; set; } 20 | public bool HasActiveChallengePopup { get; set; } 21 | public bool HasChallengeFrame { get; set; } 22 | 23 | // GeeTest-specific 24 | public string? Gt { get; set; } 25 | public string? Challenge { get; set; } 26 | public string? CaptchaId { get; set; } 27 | public string? Version { get; set; } 28 | 29 | // DataDome-specific 30 | public string? DataDomeCaptchaUrl { get; set; } 31 | public string? DataDomeCid { get; set; } 32 | public string? DataDomeHash { get; set; } 33 | public string? DataDomeUserAgent { get; set; } 34 | public string? DataDomeReferer { get; set; } 35 | 36 | public CaptchaType CaptchaType { get; set; } 37 | 38 | public Captcha() 39 | { 40 | Display = new CaptchaDisplay(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/LoadTimesTest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Utils; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 8 | { 9 | public class LoadTimesTest: BrowserDefault 10 | { 11 | [Fact] 12 | public async Task ShouldWork() 13 | { 14 | var stealthPlugin = new StealthPlugin(); 15 | var page = await LaunchAndGetPageAsync(stealthPlugin); 16 | 17 | await page.GoToAsync("https://google.com"); 18 | 19 | var loadTimes = await page.EvaluateFunctionAsync("() => window.chrome.loadTimes()"); 20 | 21 | Assert.NotNull(loadTimes); 22 | 23 | Assert.NotNull(loadTimes.GetString("connectionInfo")); 24 | Assert.NotNull(loadTimes.GetString("npnNegotiatedProtocol")); 25 | Assert.NotNull(loadTimes.GetProperty("wasAlternateProtocolAvailable")); 26 | Assert.NotNull(loadTimes.GetProperty("wasAlternateProtocolAvailable")); 27 | Assert.NotNull(loadTimes.GetProperty("wasFetchedViaSpdy")); 28 | Assert.NotNull(loadTimes.GetProperty("wasNpnNegotiated")); 29 | Assert.NotNull(loadTimes.GetProperty("firstPaintAfterLoadTime")); 30 | Assert.NotNull(loadTimes.GetProperty("requestTime")); 31 | Assert.NotNull(loadTimes.GetProperty("startLoadTime")); 32 | Assert.NotNull(loadTimes.GetProperty("commitLoadTime")); 33 | Assert.NotNull(loadTimes.GetProperty("finishDocumentLoadTime")); 34 | Assert.NotNull(loadTimes.GetProperty("firstPaintTime")); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30011.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PuppeteerExtraSharp", "PuppeteerExtraSharp\PuppeteerExtraSharp.csproj", "{B2221B32-DFAE-4F87-B0EC-EE4922B81F64}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extra.Tests", "Tests\Extra.Tests.csproj", "{FB432A25-A844-4C78-9194-E46D806B46E5}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {B2221B32-DFAE-4F87-B0EC-EE4922B81F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {B2221B32-DFAE-4F87-B0EC-EE4922B81F64}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {B2221B32-DFAE-4F87-B0EC-EE4922B81F64}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {B2221B32-DFAE-4F87-B0EC-EE4922B81F64}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {FB432A25-A844-4C78-9194-E46D806B46E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {FB432A25-A844-4C78-9194-E46D806B46E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {FB432A25-A844-4C78-9194-E46D806B46E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {FB432A25-A844-4C78-9194-E46D806B46E5}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {B538872E-C265-455D-8846-C1846F2507D7} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/PuppeteerExtraSharp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30011.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PuppeteerExtraSharp", "PuppeteerExtraSharp.csproj", "{B2221B32-DFAE-4F87-B0EC-EE4922B81F64}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extra.Tests", "..\tests\Extra.Tests.csproj", "{FB432A25-A844-4C78-9194-E46D806B46E5}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {B2221B32-DFAE-4F87-B0EC-EE4922B81F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {B2221B32-DFAE-4F87-B0EC-EE4922B81F64}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {B2221B32-DFAE-4F87-B0EC-EE4922B81F64}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {B2221B32-DFAE-4F87-B0EC-EE4922B81F64}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {FB432A25-A844-4C78-9194-E46D806B46E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {FB432A25-A844-4C78-9194-E46D806B46E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {FB432A25-A844-4C78-9194-E46D806B46E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {FB432A25-A844-4C78-9194-E46D806B46E5}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {B538872E-C265-455D-8846-C1846F2507D7} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Tests/CaptchaSolverTests/HCaptchaTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.CaptchaSolver; 3 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.CaptchaSolverTests; 8 | 9 | public class HCaptchaTests : CaptchaSolverTestsBase 10 | { 11 | [Theory] 12 | [MemberData(nameof(Providers))] 13 | public async Task ShouldSolveCheckbox(ICaptchaSolverProvider provider) 14 | { 15 | var plugin = new CaptchaSolverPlugin(provider); 16 | var page = await LaunchAndGetPageAsync(plugin); 17 | 18 | await page.GoToAsync("https://nopecha.com/demo/hcaptcha"); 19 | 20 | var result = await plugin.FindCaptchaAsync(page, new CaptchaOptions 21 | { 22 | SolveInViewportOnly = true, 23 | SolveScoreBased = false, 24 | }); 25 | 26 | Assert.Null(result.Error); 27 | Assert.NotEmpty(result.Captchas); 28 | 29 | /* var result = await plugin.SolveCaptchaAsync(page, new CaptchaSolverOptions 30 | { 31 | SolveInViewportOnly = true, 32 | SolveScoreBased = false, 33 | }); 34 | 35 | Assert.Null(result.Error); 36 | Assert.NotEmpty(result.Solved); 37 | Assert.NotEmpty(result.Filtered); 38 | Assert.All(result.Filtered, captcha => Assert.True(captcha.Captcha.IsInvisible)); 39 | 40 | await page.ClickAsync("button.form-field[type='submit']"); 41 | await Task.Delay(2000); 42 | var answerElement = await page.EvaluateExpressionAsync("document.querySelector(\"#token_3\").textContent"); 43 | Assert.Equal("success", answerElement);*/ 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/CaptchaProviderOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 4 | 5 | public class CaptchaProviderOptions 6 | { 7 | public static readonly int DefaultMaxPollingAttempts = 30; 8 | public static readonly TimeSpan DefaultStartTimeout = TimeSpan.FromSeconds(30); 9 | public static readonly TimeSpan DefaultApiTimeout = TimeSpan.FromSeconds(120); 10 | 11 | private int _maxPollingAttempts = DefaultMaxPollingAttempts; 12 | private TimeSpan _startTimeout = DefaultStartTimeout; 13 | private TimeSpan _apiTimeout = DefaultApiTimeout; 14 | 15 | /// 16 | /// Maximum number of polling attempts to remote service before stopping. 17 | /// 18 | public int MaxPollingAttempts 19 | { 20 | get => _maxPollingAttempts; 21 | set => _maxPollingAttempts = value > 0 22 | ? value 23 | : throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than 0."); 24 | } 25 | 26 | /// 27 | /// Timeout before requesting a captcha solution. 28 | /// 29 | public TimeSpan StartTimeout 30 | { 31 | get => _startTimeout; 32 | set => _startTimeout = value > TimeSpan.Zero 33 | ? value 34 | : throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than 0."); 35 | } 36 | 37 | /// 38 | /// General timeout for provider API operations. 39 | /// 40 | public TimeSpan ApiTimeout 41 | { 42 | get => _apiTimeout; 43 | set => _apiTimeout = value > TimeSpan.Zero 44 | ? value 45 | : throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than 0."); 46 | } 47 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Providers/CaptchaProviderOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers; 3 | 4 | public class CaptchaProviderOptions 5 | { 6 | public static readonly int DefaultMaxPollingAttempts = 30; 7 | public static readonly TimeSpan DefaultStartTimeout = TimeSpan.FromSeconds(30); 8 | public static readonly TimeSpan DefaultApiTimeout = TimeSpan.FromSeconds(120); 9 | 10 | private int _maxPollingAttempts = DefaultMaxPollingAttempts; 11 | private TimeSpan _startTimeout = DefaultStartTimeout; 12 | private TimeSpan _apiTimeout = DefaultApiTimeout; 13 | 14 | /// 15 | /// Maximum number of polling attempts to remote service before stopping. 16 | /// 17 | public int MaxPollingAttempts 18 | { 19 | get => _maxPollingAttempts; 20 | set => _maxPollingAttempts = value > 0 21 | ? value 22 | : throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than 0."); 23 | } 24 | 25 | /// 26 | /// Timeout before requesting a captcha solution. 27 | /// 28 | public TimeSpan StartTimeout 29 | { 30 | get => _startTimeout; 31 | set => _startTimeout = value > TimeSpan.Zero 32 | ? value 33 | : throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than 0."); 34 | } 35 | 36 | /// 37 | /// General timeout for provider API operations. 38 | /// 39 | public TimeSpan ApiTimeout 40 | { 41 | get => _apiTimeout; 42 | set => _apiTimeout = value > TimeSpan.Zero 43 | ? value 44 | : throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than 0."); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Helpers/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Utils; 5 | using PuppeteerSharp; 6 | 7 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Helpers; 8 | 9 | public static class Helpers 10 | { 11 | private static ConcurrentDictionary> Scripts { get; } = new(); 12 | 13 | public static async Task EnsureEvaluateFunctionAsync( 14 | this IPage page, 15 | string scriptName, 16 | params object[] args) 17 | { 18 | var script = ResourcesReader.ReadFile(scriptName); 19 | 20 | var pageScripts = Scripts.GetOrAdd(page, _ => new List()); 21 | 22 | lock (pageScripts) 23 | { 24 | if (pageScripts.Contains(scriptName)) return; 25 | } 26 | 27 | await page.EvaluateFunctionAsync(script, args); 28 | 29 | lock (pageScripts) 30 | { 31 | if (!pageScripts.Contains(scriptName)) 32 | { 33 | pageScripts.Add(scriptName); 34 | } 35 | } 36 | } 37 | 38 | public static async Task EnsureEvaluateExpressionOnNewDocumentAsync(this IPage page, string scriptName) 39 | { 40 | var script = ResourcesReader.ReadFile(scriptName); 41 | 42 | var pageScripts = Scripts.GetOrAdd(page, _ => new List()); 43 | 44 | lock (pageScripts) 45 | { 46 | if (pageScripts.Contains(scriptName)) return; 47 | } 48 | 49 | await page.EvaluateExpressionOnNewDocumentAsync(script); 50 | 51 | lock (pageScripts) 52 | { 53 | if (!pageScripts.Contains(scriptName)) 54 | { 55 | pageScripts.Add(scriptName); 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/README.md: -------------------------------------------------------------------------------- 1 | # PuppeteerExtraSharp 2 | 3 | [![NuGet Downloads](https://img.shields.io/nuget/dt/PuppeteerExtraSharp)](https://img.shields.io/nuget/dt/PuppeteerExtraSharp) 4 | [![NuGet Version](https://img.shields.io/nuget/v/PuppeteerExtraSharp)](https://img.shields.io/nuget/v/PuppeteerExtraSharp) 5 | 6 | PuppeteerExtraSharp is a .NET port of the [puppeteer-extra](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra) library for Node.js 7 | 8 | ## Plugins 9 | 10 | 🪄 [Puppeteer reCAPTCHA plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/Recaptcha) 11 | - Automatically handles reCAPTCHA challenges (v2, invisible, v3). 12 | 13 | 🏴 [Puppeteer stealth plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/ExtraStealth) 14 | - Applies multiple evasions to make headless automation harder to detect. 15 | 16 | 📃 [Puppeteer block resources plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/BlockResources) 17 | - Block unwanted network requests (scripts, images, documents, etc.) using simple, composable rules 18 | 19 | ## Quick Start 20 | 21 | ```csharp 22 | // Initialize plugin builder 23 | var extra = new PuppeteerExtra(); 24 | 25 | // Enable the Stealth plugin 26 | extra.Use(new StealthPlugin()); 27 | 28 | // Launch the browser with plugins applied 29 | var browser = await extra.LaunchAsync(); 30 | 31 | // Create a new page 32 | var page = await browser.NewPageAsync(); 33 | 34 | await page.GoToAsync("https://google.com"); 35 | await Task.Delay(2000); 36 | 37 | // Take a screenshot 38 | await page.ScreenshotAsync("extra.png"); 39 | ``` 40 | 41 | ## Notes 42 | - Use the reCAPTCHA plugin only on properties you own or where you have explicit permission to automate. 43 | - Some targets may still detect automation; adjust plugin combinations and browser settings as needed. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PuppeteerExtraSharp 2 | 3 | [![NuGet Downloads](https://img.shields.io/nuget/dt/PuppeteerExtraSharp)](https://www.nuget.org/packages/PuppeteerExtraSharp) 4 | [![NuGet Version](https://img.shields.io/nuget/v/PuppeteerExtraSharp)](https://www.nuget.org/packages/PuppeteerExtraSharp) 5 | 6 | PuppeteerExtraSharp is a .NET port of the [puppeteer-extra](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra) library for Node.js 7 | 8 | ## Plugins 9 | 10 | 🪄 [Puppeteer captcha solver plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/CaptchaSolver) 11 | - Automatically handles reCAPTCHA challenges (v2, invisible, v3), GeeTest, CloudFlare. 12 | 13 | 🏴 [Puppeteer stealth plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/ExtraStealth) 14 | - Applies multiple evasions to make headless automation harder to detect. 15 | 16 | 📃 [Puppeteer block resources plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/BlockResources) 17 | - Block unwanted network requests (scripts, images, documents, etc.) using simple, composable rules 18 | 19 | ## Quick Start 20 | 21 | ```csharp 22 | // Initialize plugin builder 23 | var extra = new PuppeteerExtra(); 24 | 25 | // Enable the Stealth plugin 26 | extra.Use(new StealthPlugin()); 27 | 28 | // Launch the browser with plugins applied 29 | var browser = await extra.LaunchAsync(); 30 | 31 | // Create a new page 32 | var page = await browser.NewPageAsync(); 33 | 34 | await page.GoToAsync("https://google.com"); 35 | await Task.Delay(2000); 36 | 37 | // Take a screenshot 38 | await page.ScreenshotAsync("extra.png"); 39 | ``` 40 | 41 | ## Notes 42 | - Use the captcha solver plugin only on properties you own or where you have explicit permission to automate. 43 | - Some targets may still detect automation; adjust plugin combinations and browser settings as needed. 44 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Helpers/ScriptHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using PuppeteerSharp; 5 | 6 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Helpers; 7 | 8 | public static class ScriptHelper 9 | { 10 | private static Dictionary> Scripts { get; } = new(); 11 | 12 | public static async Task EnsureEvaluateFunctionAsync(this IPage page, 13 | string scriptName, 14 | string script, 15 | params object[] args) 16 | { 17 | // If we think we've already injected this script for the page, double‑check 18 | // that the expected globals still exist (the JS context is reset on navigation). 19 | if (Scripts.ContainsKey(page) && Scripts[page].Contains(scriptName)) 20 | { 21 | // Heuristic: our Recaptcha script defines window.reScript 22 | if (scriptName.EndsWith("RecaptchaScript.js", StringComparison.OrdinalIgnoreCase)) 23 | { 24 | try 25 | { 26 | var hasGlobal = await page.EvaluateExpressionAsync("typeof window.reScript !== 'undefined'"); 27 | if (!hasGlobal) 28 | { 29 | // Context was lost; re-inject the function 30 | await page.EvaluateFunctionAsync(script, args); 31 | } 32 | } 33 | catch 34 | { 35 | // If evaluation fails (e.g., context not ready), fall back to injecting 36 | await page.EvaluateFunctionAsync(script, args); 37 | } 38 | } 39 | 40 | return; 41 | } 42 | 43 | if (!Scripts.ContainsKey(page)) 44 | { 45 | Scripts.Add(page, new List()); 46 | } 47 | 48 | await page.EvaluateFunctionAsync(script, args); 49 | Scripts[page].Add(scriptName); 50 | } 51 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/ChromeSciTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Extra.Tests.Utils; 3 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 7 | { 8 | public class ChromeSciTest : BrowserDefault 9 | { 10 | [Fact] 11 | public async Task ShouldWork() 12 | { 13 | var plugin = new StealthPlugin(); 14 | var page = await LaunchAndGetPageAsync(plugin); 15 | await page.GoToAsync("https://google.com"); 16 | var sci = (await page.EvaluateFunctionAsync(@"() => { 17 | const { timing } = window.performance 18 | const csi = window.chrome.csi() 19 | return { 20 | csi: { 21 | exists: window.chrome && 'csi' in window.chrome, 22 | toString: chrome.csi.toString() 23 | }, 24 | dataOK: { 25 | onloadT: csi.onloadT === timing.domContentLoadedEventEnd, 26 | startE: csi.startE === timing.navigationStart, 27 | pageT: Number.isFinite(csi.pageT), 28 | tran: Number.isFinite(csi.tran) 29 | } 30 | } 31 | }")).Value; 32 | 33 | Assert.True(sci.GetProperty("csi").GetBoolean("exists")); 34 | Assert.Equal("function () { [native code] }", sci.GetProperty("csi").GetString("toString"));; 35 | 36 | var dataProp = sci.GetProperty("dataOK"); 37 | Assert.True(dataProp.GetBoolean("onloadT")); 38 | Assert.True(dataProp.GetBoolean("pageT")); 39 | Assert.True(dataProp.GetBoolean("startE")); 40 | Assert.True(dataProp.GetBoolean("tran")); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/BlockResourcesPlugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using PuppeteerExtraSharp.Plugins.BlockResources.Rules; 6 | using PuppeteerSharp; 7 | 8 | namespace PuppeteerExtraSharp.Plugins.BlockResources; 9 | 10 | public class BlockResourcesPlugin( 11 | RequestAbortErrorCode? abortErrorCode = null, 12 | int? priority = null) : PuppeteerExtraPlugin("block-resources") 13 | { 14 | private readonly List _blockRules = []; 15 | 16 | public IBlockRule AddRule(Action builderAction) 17 | { 18 | var builder = new ResourcesBlockBuilder(); 19 | builderAction(builder); 20 | 21 | var rule = builder.Build(); 22 | _blockRules.Add(rule); 23 | return rule; 24 | } 25 | 26 | public IBlockRule AddRule(IBlockRule rule) 27 | { 28 | _blockRules.Add(rule); 29 | return rule; 30 | } 31 | 32 | public BlockResourcesPlugin RemoveRule(IBlockRule rule) 33 | { 34 | _blockRules.Remove(rule); 35 | return this; 36 | } 37 | 38 | 39 | protected internal override async Task OnPageCreatedAsync(IPage page) 40 | { 41 | await page.SetRequestInterceptionAsync(true); 42 | page.AddRequestInterceptor(OnPageRequestAsync); 43 | } 44 | 45 | 46 | private async Task OnPageRequestAsync(IRequest req) 47 | { 48 | if (_blockRules.Any(rule => rule.ShouldBlock(req))) 49 | { 50 | await req.AbortAsync(abortErrorCode ?? RequestAbortErrorCode.BlockedByClient, priority); 51 | return; 52 | } 53 | 54 | await req.ContinueAsync(); 55 | } 56 | 57 | 58 | protected internal override Task BeforeLaunchAsync(LaunchOptions options) 59 | { 60 | options.Args = options.Args.Append("--site-per-process").Append("--disable-features=IsolateOrigins") 61 | .ToArray(); 62 | 63 | return Task.CompletedTask; 64 | } 65 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/ApiClient/ApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Net.Http.Json; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using PuppeteerExtraSharp.Utils; 8 | 9 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.ApiClient; 10 | 11 | public class ApiClient 12 | { 13 | private readonly HttpClient _client; 14 | 15 | public ApiClient(string baseUrl = null) 16 | { 17 | _client = new HttpClient(); 18 | 19 | if (string.IsNullOrWhiteSpace(baseUrl)) return; 20 | 21 | _client.BaseAddress = new Uri(baseUrl); 22 | } 23 | 24 | public PollingRequest CreatePostPollingRequest(string url, object content) 25 | { 26 | return new PollingRequest(_client, () => 27 | { 28 | var request = new HttpRequestMessage(HttpMethod.Post, url); 29 | request.Content = JsonContent.Create(content); 30 | return request; 31 | }); 32 | } 33 | 34 | public async Task PostAsync( 35 | string url, 36 | object content, 37 | CancellationToken token = default) 38 | { 39 | var data = JsonContent.Create(content); 40 | var response = await _client.PostAsync(url, data, token); 41 | response.EnsureSuccessStatusCode(); 42 | return await response.Content.ReadFromJsonAsync(cancellationToken: token); 43 | } 44 | 45 | private Uri CreateUri(string url, Dictionary query) 46 | { 47 | if (!Uri.TryCreate(url, UriKind.Absolute, out var fullUri)) 48 | { 49 | if (_client.BaseAddress == null) 50 | throw new InvalidOperationException("BaseAddress is not initialized"); 51 | 52 | fullUri = new Uri(_client.BaseAddress, url); 53 | } 54 | 55 | var uriBuilder = new UriBuilder(fullUri) 56 | { 57 | Query = QueryHelper.ToQuery(query) 58 | }; 59 | 60 | return uriBuilder.Uri; 61 | } 62 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/CodecTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Extra.Tests.Utils; 3 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 7 | { 8 | public class CodecTest : BrowserDefault 9 | { 10 | [Fact] 11 | public async Task SupportsCodec() 12 | { 13 | var plugin = new StealthPlugin(); 14 | var page = await LaunchAndGetPageAsync(plugin); 15 | await page.GoToAsync("https://google.com"); 16 | var fingerPrint = await new FingerPrint().GetFingerPrint(page); 17 | 18 | var videoCodec = fingerPrint.GetProperty("videoCodecs"); 19 | // Assert.Equal("probably", videoCodec.GetString("ogg")); 20 | Assert.Equal("probably", videoCodec.GetString("h264")); 21 | Assert.Equal("probably", videoCodec.GetString("webm")); 22 | 23 | var audioCodec = fingerPrint.GetProperty("audioCodecs"); 24 | Assert.Equal("probably", audioCodec.GetString("ogg")); 25 | Assert.Equal("probably", audioCodec.GetString("mp3")); 26 | Assert.Equal("probably", audioCodec.GetString("wav")); 27 | Assert.Equal("maybe", audioCodec.GetString("m4a")); 28 | Assert.Equal("probably", audioCodec.GetString("aac")); 29 | } 30 | 31 | [Fact] 32 | public async Task NotLeakModifications() 33 | { 34 | var plugin = new StealthPlugin(); 35 | var page = await LaunchAndGetPageAsync(plugin); 36 | 37 | var canPlay = 38 | await page.EvaluateFunctionAsync( 39 | "() => document.createElement('audio').canPlayType.toString()"); 40 | Assert.Equal("function canPlayType() { [native code] }", canPlay); 41 | 42 | var canPlayHasName = await page.EvaluateFunctionAsync( 43 | "() => document.createElement('audio').canPlayType.name"); 44 | Assert.Equal("canPlayType", canPlayHasName); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/RuntimeTest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Threading.Tasks; 3 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 7 | { 8 | public class RuntimeTest : BrowserDefault 9 | { 10 | [Fact] 11 | public async Task ShouldAddConnectToChrome() 12 | { 13 | var plugin = new StealthPlugin(); 14 | var page = await LaunchAndGetPageAsync(plugin); 15 | 16 | await page.GoToAsync("https://google.com"); 17 | 18 | var runtime = await page.EvaluateExpressionAsync("chrome.runtime"); 19 | Assert.NotNull(runtime); 20 | 21 | var runtimeConnect = await page.EvaluateExpressionAsync("chrome.runtime.connect"); 22 | Assert.NotNull(runtimeConnect); 23 | 24 | var runtimeName = await page.EvaluateExpressionAsync("chrome.runtime.connect.name"); 25 | Assert.Equal("connect", runtimeName); 26 | 27 | var sendMessage = await page.EvaluateExpressionAsync("chrome.runtime.sendMessage.name"); 28 | Assert.NotNull(sendMessage); 29 | 30 | var sendMessageUndefined = await page.EvaluateExpressionAsync("chrome.runtime.sendMessage('nckgahadagoaajjgafhacjanaoiihapd', '') === undefined"); 31 | Assert.True(sendMessageUndefined); 32 | 33 | var validIdWorks = await page.EvaluateExpressionAsync("chrome.runtime.connect('nckgahadagoaajjgafhacjanaoiihapd') !== undefined"); 34 | Assert.True(validIdWorks); 35 | 36 | var nestedToString = await page.EvaluateExpressionAsync("chrome.runtime.connect('nckgahadagoaajjgafhacjanaoiihapd').onDisconnect.addListener + ''"); 37 | Assert.Equal("function addListener() { [native code] }", nestedToString); 38 | 39 | var noReturn = await page.EvaluateExpressionAsync("chrome.runtime.connect('nckgahadagoaajjgafhacjanaoiihapd').disconnect() === undefined"); 40 | Assert.True(noReturn); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/readme.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | ```c# 4 | var extra = new PuppeteerExtra(); 5 | // initialize stealth plugin 6 | var stealth = new StealthPlugin(); 7 | 8 | var browser = await extra.Use(stealth).LaunchAsync(new LaunchOptions()); 9 | 10 | var page = await browser.NewPageAsync(); 11 | 12 | await page.GoToAsync("https://bot.sannysoft.com/"); 13 | ``` 14 | 15 | # Plugin options 16 | 17 | ```c# 18 | var extra = new PuppeteerExtra(); 19 | // Initialize hardware concurrency plugin options 20 | var stealthHardwareConcurrencyOptions = new StealthHardwareConcurrencyOptions(12); 21 | // Put it on stealth plugin constructor 22 | var stealth = new StealthPlugin(stealthHardwareConcurrencyOptions); 23 | var browser = await extra.Use(stealth).LaunchAsync(new LaunchOptions()); 24 | var page = await browser.NewPageAsync(); 25 | await page.GoToAsync("https://bot.sannysoft.com/"); 26 | ``` 27 | 28 | ### Available options: 29 | 30 | #### Hardware concurrency 31 | 32 | see (https://arh.antoinevastel.com/reports/stats/osName_hardwareConcurrency_report.html) 33 | 34 | ```c# 35 | var concurrency = 12; // your number 36 | var stealthHardwareConcurrencyOptions = new StealthHardwareConcurrencyOptions(concurrency); 37 | ``` 38 | 39 | #### Vendor 40 | 41 | ```c# 42 | var vendor = "Google Inc."; // your custom navigator.vendor 43 | var stealthVendorSettings = new StealthVendorSettings(vendor); 44 | ``` 45 | 46 | ### Languages 47 | 48 | ```c# 49 | var languages = "en-US"; // your custom languages array 50 | var languagesSettings = new StealthLanguagesOptions(languages); 51 | ``` 52 | 53 | ### WebGL 54 | 55 | ```c# 56 | var webGLVendor = "Intel Inc."; // your custom webGL vendor 57 | var render = "Intel Iris OpenGL Engine"; // your custom webGL renderer 58 | var languagesSettings = new StealthWebGLOptions(webGLVendor, render); 59 | ``` 60 | 61 | # Removing evasions: 62 | 63 | You can remove an evasion from the plugin by using the RemoveEvasionByType 64 | 65 | ```c# 66 | var extra = new PuppeteerExtra(); 67 | var stealth = new StealthPlugin(); 68 | stealthPlugin.RemoveEvasion("stealth-webDriver"); 69 | var browser = await extra.Use(stealth).LaunchAsync(); 70 | ``` -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/Codec.js: -------------------------------------------------------------------------------- 1 | () => { 2 | /** 3 | * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing. 4 | * 5 | * @example 6 | * video/webm; codecs="vp8, vorbis" 7 | * video/mp4; codecs="avc1.42E01E" 8 | * audio/x-m4a; 9 | * audio/ogg; codecs="vorbis" 10 | * @param {String} arg 11 | */ 12 | const parseInput = arg => { 13 | const [mime, codecStr] = arg.trim().split(';') 14 | let codecs = [] 15 | if (codecStr && codecStr.includes('codecs="')) { 16 | codecs = codecStr 17 | .trim() 18 | .replace(`codecs="`, '') 19 | .replace(`"`, '') 20 | .trim() 21 | .split(',') 22 | .filter(x => !!x) 23 | .map(x => x.trim()) 24 | } 25 | return { 26 | mime, 27 | codecStr, 28 | codecs 29 | } 30 | } 31 | 32 | const canPlayType = { 33 | // Intercept certain requests 34 | apply: function (target, ctx, args) { 35 | if (!args || !args.length) { 36 | return target.apply(ctx, args) 37 | } 38 | const {mime, codecs} = parseInput(args[0]) 39 | // This specific mp4 codec is missing in Chromium 40 | if (mime === 'video/mp4') { 41 | if (codecs.includes('avc1.42E01E')) { 42 | return 'probably' 43 | } 44 | } 45 | // This mimetype is only supported if no codecs are specified 46 | if (mime === 'audio/x-m4a' && !codecs.length) { 47 | return 'maybe' 48 | } 49 | 50 | // This mimetype is only supported if no codecs are specified 51 | if (mime === 'audio/aac' && !codecs.length) { 52 | return 'probably' 53 | } 54 | // Everything else as usual 55 | return target.apply(ctx, args) 56 | } 57 | } 58 | 59 | /* global HTMLMediaElement */ 60 | utils.replaceWithProxy( 61 | HTMLMediaElement.prototype, 62 | 'canPlayType', 63 | canPlayType 64 | ) 65 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/PuppeteerExtraPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins; 6 | 7 | public abstract class PuppeteerExtraPlugin(string pluginName) 8 | { 9 | public string Name { get; } = pluginName; 10 | 11 | public virtual List Requirements { get; } = new(); 12 | 13 | protected internal virtual ICollection GetDependencies() 14 | { 15 | return null; 16 | } 17 | 18 | protected internal virtual Task BeforeLaunchAsync(LaunchOptions options) 19 | { 20 | return Task.CompletedTask; 21 | } 22 | 23 | protected internal virtual Task AfterLaunchAsync(IBrowser browser) 24 | { 25 | return Task.CompletedTask; 26 | } 27 | 28 | protected internal virtual Task BeforeConnectAsync(ConnectOptions options) 29 | { 30 | return Task.CompletedTask; 31 | } 32 | 33 | protected internal virtual Task AfterConnectAsync(IBrowser browser) 34 | { 35 | return Task.CompletedTask; 36 | } 37 | 38 | protected internal virtual Task OnBrowserAsync(IBrowser browser) 39 | { 40 | return Task.CompletedTask; 41 | } 42 | 43 | protected internal virtual Task OnTargetCreatedAsync(Target target) 44 | { 45 | return Task.CompletedTask; 46 | } 47 | 48 | protected internal virtual Task OnPageCreatedAsync(IPage page) 49 | { 50 | return Task.CompletedTask; 51 | } 52 | 53 | protected internal virtual Task OnTargetChangedAsync(Target target) 54 | { 55 | return Task.CompletedTask; 56 | } 57 | 58 | protected internal virtual Task OnTargetDestroyedAsync(Target target) 59 | { 60 | return Task.CompletedTask; 61 | } 62 | 63 | protected internal virtual Task OnDisconnectedAsync() 64 | { 65 | return Task.CompletedTask; 66 | } 67 | 68 | protected internal virtual Task OnCloseAsync() 69 | { 70 | return Task.CompletedTask; 71 | } 72 | 73 | protected internal virtual Task OnPluginRegisteredAsync() 74 | { 75 | return Task.CompletedTask; 76 | } 77 | } 78 | 79 | public enum PluginRequirements 80 | { 81 | Launch, 82 | HeadFul, 83 | RunLast 84 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/ApiClient/PollingRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Json; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.ApiClient; 7 | 8 | public class PollingRequest(HttpClient client, Func requestFactory) 9 | { 10 | private const int DefaultPollIntervalSeconds = 5; 11 | private const int DefaultMaxAttempts = 5; 12 | 13 | private int _timeout = DefaultPollIntervalSeconds; 14 | private int _limit = DefaultMaxAttempts; 15 | 16 | public PollingRequest WithTimeoutSeconds(int timeout) 17 | { 18 | if (timeout <= 0) throw new ArgumentOutOfRangeException(nameof(timeout), "Timeout must be positive."); 19 | _timeout = timeout; 20 | return this; 21 | } 22 | 23 | public PollingRequest TriesLimit(int limit) 24 | { 25 | if (limit <= 0) throw new ArgumentOutOfRangeException(nameof(limit), "Limit must be positive."); 26 | _limit = limit; 27 | return this; 28 | } 29 | 30 | public async Task> ActivatePollingAsync( 31 | Func, PollingAction> decide, CancellationToken cancellation) 32 | { 33 | while (true) 34 | { 35 | cancellation.ThrowIfCancellationRequested(); 36 | 37 | using var request = requestFactory(); 38 | using var response = await client.SendAsync(request, cancellation).ConfigureAwait(false); 39 | 40 | var apiResponse = new ApiResponse 41 | { 42 | Data = await response.Content.ReadFromJsonAsync(cancellationToken: cancellation).ConfigureAwait(false), 43 | StatusCode = response.StatusCode 44 | }; 45 | 46 | if (decide(apiResponse) == PollingAction.Break || _limit <= 1) 47 | return apiResponse; 48 | 49 | _limit -= 1; 50 | await Task.Delay(TimeSpan.FromSeconds(_timeout), cancellation).ConfigureAwait(false); 51 | } 52 | } 53 | } 54 | 55 | public class ApiResponse 56 | { 57 | public T? Data { get; set; } 58 | public System.Net.HttpStatusCode StatusCode { get; set; } 59 | } 60 | 61 | public enum PollingAction 62 | { 63 | ContinuePolling, 64 | Break 65 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/ApiClient/PollingRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Json; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.ApiClient; 8 | 9 | public class PollingRequest(HttpClient client, Func requestFactory) 10 | { 11 | private const int DefaultPollIntervalSeconds = 5; 12 | private const int DefaultMaxAttempts = 5; 13 | 14 | private int _timeout = DefaultPollIntervalSeconds; 15 | private int _limit = DefaultMaxAttempts; 16 | 17 | public PollingRequest WithTimeoutSeconds(int timeout) 18 | { 19 | if (timeout <= 0) throw new ArgumentOutOfRangeException(nameof(timeout), "Timeout must be positive."); 20 | _timeout = timeout; 21 | return this; 22 | } 23 | 24 | public PollingRequest TriesLimit(int limit) 25 | { 26 | if (limit <= 0) throw new ArgumentOutOfRangeException(nameof(limit), "Limit must be positive."); 27 | _limit = limit; 28 | return this; 29 | } 30 | 31 | public async Task> ActivatePollingAsync( 32 | Func, PollingAction> decide, CancellationToken cancellation) 33 | { 34 | while (true) 35 | { 36 | cancellation.ThrowIfCancellationRequested(); 37 | 38 | using var request = requestFactory(); 39 | using var response = await client.SendAsync(request, cancellation).ConfigureAwait(false); 40 | 41 | var apiResponse = new ApiResponse 42 | { 43 | Data = await response.Content.ReadFromJsonAsync(cancellationToken: cancellation).ConfigureAwait(false), 44 | StatusCode = response.StatusCode 45 | }; 46 | 47 | if (decide(apiResponse) == PollingAction.Break || _limit <= 1) 48 | return apiResponse; 49 | 50 | _limit -= 1; 51 | await Task.Delay(TimeSpan.FromSeconds(_timeout), cancellation).ConfigureAwait(false); 52 | } 53 | } 54 | } 55 | 56 | public class ApiResponse 57 | { 58 | public T? Data { get; set; } 59 | public System.Net.HttpStatusCode StatusCode { get; set; } 60 | } 61 | 62 | public enum PollingAction 63 | { 64 | ContinuePolling, 65 | Break 66 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/ApiClient/ApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Net.Http.Json; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using PuppeteerExtraSharp.Utils; 8 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.ApiClient; 9 | 10 | public class ApiClient 11 | { 12 | private readonly HttpClient _client; 13 | 14 | public ApiClient(string baseUrl = null) 15 | { 16 | _client = new HttpClient(); 17 | 18 | if (string.IsNullOrWhiteSpace(baseUrl)) return; 19 | 20 | _client.BaseAddress = new Uri(baseUrl); 21 | } 22 | 23 | public PollingRequest CreatePostPollingRequest(string url, object content) 24 | { 25 | return new PollingRequest(_client, () => 26 | { 27 | var request = new HttpRequestMessage(HttpMethod.Post, url); 28 | request.Content = JsonContent.Create(content); 29 | return request; 30 | }); 31 | } 32 | 33 | public async Task PostAsync( 34 | string url, 35 | object content, 36 | CancellationToken token = default) 37 | { 38 | var data = JsonContent.Create(content); 39 | var response = await _client.PostAsync(url, data, token); 40 | 41 | if (!response.IsSuccessStatusCode) 42 | { 43 | var errorBody = await response.Content.ReadAsStringAsync(token); 44 | throw new HttpRequestException( 45 | $"Response status code does not indicate success: {(int)response.StatusCode} ({response.ReasonPhrase}). Response: {errorBody}"); 46 | } 47 | 48 | return await response.Content.ReadFromJsonAsync(cancellationToken: token); 49 | } 50 | 51 | private Uri CreateUri(string url, Dictionary query) 52 | { 53 | if (!Uri.TryCreate(url, UriKind.Absolute, out var fullUri)) 54 | { 55 | if (_client.BaseAddress == null) 56 | throw new InvalidOperationException("BaseAddress is not initialized"); 57 | 58 | fullUri = new Uri(_client.BaseAddress, url); 59 | } 60 | 61 | var uriBuilder = new UriBuilder(fullUri) 62 | { 63 | Query = QueryHelper.ToQuery(query) 64 | }; 65 | 66 | return uriBuilder.Uri; 67 | } 68 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Models/RecaptchaSolveOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Models; 4 | 5 | public class RecaptchaSolveOptions 6 | { 7 | // Defaults 8 | private const double DefaultMinV3Score = 0.5; 9 | private static readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromSeconds(10); 10 | 11 | /// 12 | /// Visualize reCAPTCHAs based on their state. 13 | /// TODO: NOT IMPLEMENTED 14 | /// 15 | public bool VisualFeedback { get; set; } = false; 16 | 17 | /// 18 | /// Throw on errors instead of returning them in the error property. 19 | /// 20 | public bool ThrowOnError { get; set; } = false; 21 | 22 | /// 23 | /// Only solve captchas and challenges visible in the viewport. 24 | /// 25 | public bool SolveInViewportOnly { get; set; } = false; 26 | 27 | /// 28 | /// Solve invisible captchas used to acquire a score and not present a challenge (e.g. reCAPTCHA v3). 29 | /// 30 | public bool SolveScoreBased { get; set; } = true; 31 | 32 | /// 33 | /// Solve invisible captchas that have no active challenge. 34 | /// 35 | public bool SolveInactiveChallenges { get; set; } = true; 36 | 37 | /// 38 | /// Solve invisible challenges (checkbox not shown) when present. 39 | /// 40 | public bool SolveInvisibleChallenges { get; set; } = true; 41 | 42 | private double _minV3RecaptchaScore = DefaultMinV3Score; 43 | 44 | /// 45 | /// Minimal acceptable score for reCAPTCHA v3 (range 0..1). Default: 0.5. 46 | /// 47 | public double MinV3RecaptchaScore 48 | { 49 | get => _minV3RecaptchaScore; 50 | set 51 | { 52 | if (value < 0 || value > 1) 53 | throw new ArgumentOutOfRangeException(nameof(MinV3RecaptchaScore), "Value must be in range [0, 1]."); 54 | _minV3RecaptchaScore = value; 55 | } 56 | } 57 | 58 | /// 59 | /// Enable verbose debug logging. 60 | /// 61 | public bool Debug { get; set; } = false; 62 | 63 | /// 64 | /// Maximum time to wait for captcha to appear/solve. Default: 10 seconds. 65 | /// 66 | public TimeSpan CaptchaWaitTimeout { get; set; } = DefaultWaitTimeout; 67 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/ChromeAppTest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Utils; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 5 | using PuppeteerSharp; 6 | using Xunit; 7 | 8 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 9 | { 10 | public class ChromeAppTest : BrowserDefault 11 | { 12 | [Fact] 13 | public async Task ShouldWork() 14 | { 15 | var plugin = new StealthPlugin(); 16 | 17 | var page = await LaunchAndGetPageAsync(plugin); 18 | await page.GoToAsync("https://google.com"); 19 | 20 | var test = await page.EvaluateExpressionAsync("window.chrome"); 21 | var chrome = await page.EvaluateExpressionAsync("window.chrome"); 22 | Assert.NotNull(chrome); 23 | 24 | var app = await page.EvaluateExpressionAsync("chrome.app"); 25 | Assert.NotNull(app); 26 | 27 | var getIsInstalled = await page.EvaluateExpressionAsync("chrome.app.getIsInstalled()"); 28 | Assert.False(getIsInstalled); 29 | 30 | var installState = await page.EvaluateExpressionAsync("chrome.app.InstallState"); 31 | Assert.NotNull(installState); 32 | 33 | Assert.Equal("disabled", installState.GetString("DISABLED")); 34 | Assert.Equal("installed", installState.GetString("INSTALLED")); 35 | Assert.Equal("not_installed", installState.GetString("NOT_INSTALLED")); 36 | 37 | var runningState = await page.EvaluateExpressionAsync("chrome.app.RunningState"); 38 | Assert.NotNull(runningState); 39 | Assert.Equal("cannot_run", runningState.GetString("CANNOT_RUN")); 40 | Assert.Equal("ready_to_run", runningState.GetString("READY_TO_RUN")); 41 | Assert.Equal("running", runningState.GetString("RUNNING"));; 42 | 43 | var details = await page.EvaluateExpressionAsync("chrome.app.getDetails()"); 44 | Assert.Null(details); 45 | 46 | var runningStateFunc = await page.EvaluateExpressionAsync("chrome.app.runningState()"); 47 | Assert.Equal("cannot_run", runningStateFunc); 48 | 49 | 50 | await Assert.ThrowsAsync(async () => await page.EvaluateExpressionAsync("chrome.app.getDetails('foo')")); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/Extra.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | Always 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | 33 | 34 | 35 | 36 | 37 | True 38 | True 39 | Resources.resx 40 | 41 | 42 | True 43 | True 44 | Resources.resx 45 | 46 | 47 | 48 | 49 | 50 | ResXFileCodeGenerator 51 | Resources.Designer.cs 52 | 53 | 54 | ResXFileCodeGenerator 55 | Resources.Designer.cs 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Vendors/Google/GoogleOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 3 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Vendors.Google; 4 | 5 | public class GoogleOptions : ICaptchaSolveOptions 6 | { 7 | // Defaults 8 | private const double DefaultMinV3Score = 0.5; 9 | private static readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromSeconds(10); 10 | 11 | /// 12 | /// Visualize reCAPTCHAs based on their state. 13 | /// TODO: NOT IMPLEMENTED 14 | /// 15 | public bool VisualFeedback { get; set; } = false; 16 | 17 | /// 18 | /// Throw on errors instead of returning them in the error property. 19 | /// 20 | public bool ThrowOnError { get; set; } = false; 21 | 22 | /// 23 | /// Only solve captchas and challenges visible in the viewport. 24 | /// 25 | public bool SolveInViewportOnly { get; set; } = false; 26 | 27 | /// 28 | /// Solve invisible captchas used to acquire a score and not present a challenge (e.g. reCAPTCHA v3). 29 | /// 30 | public bool SolveScoreBased { get; set; } = true; 31 | 32 | /// 33 | /// Solve invisible captchas that have no active challenge. 34 | /// 35 | public bool SolveInactiveChallenges { get; set; } = true; 36 | 37 | /// 38 | /// Solve invisible challenges (checkbox not shown) when present. 39 | /// 40 | public bool SolveInvisibleChallenges { get; set; } = true; 41 | 42 | private double _minV3RecaptchaScore = DefaultMinV3Score; 43 | 44 | /// 45 | /// Minimal acceptable score for reCAPTCHA v3 (range 0..1). Default: 0.5. 46 | /// 47 | public double MinV3RecaptchaScore 48 | { 49 | get => _minV3RecaptchaScore; 50 | set 51 | { 52 | if (value < 0 || value > 1) 53 | throw new ArgumentOutOfRangeException(nameof(MinV3RecaptchaScore), "Value must be in range [0, 1]."); 54 | _minV3RecaptchaScore = value; 55 | } 56 | } 57 | 58 | /// 59 | /// Enable verbose debug logging. 60 | /// 61 | public bool Debug { get; set; } = false; 62 | 63 | /// 64 | /// Maximum time to wait for captcha to appear/solve. Default: 10 seconds. 65 | /// 66 | public TimeSpan CaptchaWaitTimeout { get; set; } = DefaultWaitTimeout; 67 | } 68 | -------------------------------------------------------------------------------- /Tests/BrowserDefault.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using PuppeteerExtraSharp; 6 | using PuppeteerExtraSharp.Plugins; 7 | using PuppeteerSharp; 8 | 9 | namespace Extra.Tests 10 | { 11 | public abstract class BrowserDefault : IDisposable 12 | { 13 | private readonly List _launchedBrowsers = new List(); 14 | 15 | protected async Task LaunchAsync(LaunchOptions options = null) 16 | { 17 | // await DownloadChromeIfNotExistsAsync(); 18 | options ??= CreateDefaultOptions(); 19 | 20 | var browser = await Puppeteer.LaunchAsync(options); 21 | _launchedBrowsers.Add(browser); 22 | return browser; 23 | } 24 | 25 | protected async Task LaunchWithPluginAsync(PuppeteerExtraPlugin plugin, LaunchOptions options = null) 26 | { 27 | var extra = new PuppeteerExtra().Use(plugin); 28 | // await DownloadChromeIfNotExistsAsync(); 29 | options ??= CreateDefaultOptions(); 30 | 31 | var browser = await extra.LaunchAsync(options); 32 | _launchedBrowsers.Add(browser); 33 | return browser; 34 | } 35 | 36 | protected async Task LaunchAndGetPageAsync(PuppeteerExtraPlugin plugin = null) 37 | { 38 | IBrowser browser = null; 39 | if (plugin != null) 40 | browser = await LaunchWithPluginAsync(plugin); 41 | else 42 | browser = await LaunchAsync(); 43 | 44 | var page = (await browser.PagesAsync())[0]; 45 | 46 | return page; 47 | } 48 | 49 | 50 | private async Task DownloadChromeIfNotExistsAsync() 51 | { 52 | if (File.Exists(Constants.PathToChrome)) 53 | return; 54 | 55 | await new BrowserFetcher(new BrowserFetcherOptions() 56 | { 57 | Path = Constants.PathToChrome 58 | }).DownloadAsync(); 59 | } 60 | 61 | protected LaunchOptions CreateDefaultOptions() 62 | { 63 | return new LaunchOptions() 64 | { 65 | ExecutablePath = Constants.PathToChrome, 66 | Headless = Constants.Headless 67 | }; 68 | } 69 | 70 | public void Dispose() 71 | { 72 | foreach (var launchedBrowser in _launchedBrowsers) 73 | { 74 | launchedBrowser.CloseAsync().Wait(); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/CaptchaSolverTests/GeeTestTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using PuppeteerExtraSharp.Plugins.CaptchaSolver; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 5 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 6 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 7 | using Xunit; 8 | 9 | namespace Extra.Tests.CaptchaSolverTests; 10 | 11 | public class GeeTestTests : CaptchaSolverTestsBase 12 | { 13 | [Theory] 14 | [MemberData(nameof(Providers))] 15 | public async Task ShouldSolveV3(ICaptchaSolverProvider provider) 16 | { 17 | var plugin = new CaptchaSolverPlugin(provider); 18 | var page = await LaunchAndGetPageAsync(plugin); 19 | 20 | await page.GoToAsync("https://2captcha.com/fr/demo/geetest"); 21 | 22 | var result = await plugin.SolveCaptchaAsync(page, new CaptchaOptions 23 | { 24 | SolveInViewportOnly = true, 25 | SolveScoreBased = false, 26 | EnabledVendors = [CaptchaVendor.GeeTest], 27 | CaptchaWaitTimeout = TimeSpan.FromSeconds(20) 28 | }); 29 | 30 | Assert.Null(result.Error); 31 | Assert.NotEmpty(result.Solved); 32 | Assert.Empty(result.Filtered); 33 | 34 | await page.ClickAsync("#geetest-demo-form button[type='submit']"); 35 | await Task.Delay(2000); 36 | var answerElement = await page.EvaluateExpressionAsync("document.querySelector(\"#geetest-demo-form code\")?.textContent"); 37 | 38 | Assert.Contains("\"success\": true", answerElement); 39 | } 40 | 41 | [Theory] 42 | [MemberData(nameof(Providers))] 43 | public async Task ShouldSolveV4(ICaptchaSolverProvider provider) 44 | { 45 | var plugin = new CaptchaSolverPlugin(provider); 46 | var page = await LaunchAndGetPageAsync(plugin); 47 | 48 | await page.GoToAsync("https://2captcha.com/demo/geetest-v4"); 49 | 50 | var result = await plugin.SolveCaptchaAsync(page, new CaptchaOptions 51 | { 52 | SolveInViewportOnly = true, 53 | SolveScoreBased = false, 54 | CaptchaWaitTimeout = TimeSpan.FromSeconds(20) 55 | }); 56 | 57 | Assert.Null(result.Error); 58 | Assert.NotEmpty(result.Solved); 59 | Assert.Empty(result.Filtered); 60 | 61 | await page.ClickAsync("button[data-action=demo_action][type=submit]"); 62 | await Task.Delay(2000); 63 | var answerElement = await page.EvaluateExpressionAsync("document.querySelector(\"code\")?.textContent"); 64 | 65 | Assert.Contains("\"result\": \"success\"", answerElement); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/StealthPlugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 6 | using PuppeteerSharp; 7 | 8 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth; 9 | 10 | public class StealthPlugin : PuppeteerExtraPlugin 11 | { 12 | private readonly List _options; 13 | private readonly List _standardEvasions; 14 | 15 | public StealthPlugin(params IPuppeteerExtraPluginOptions[] options) : base("stealth") 16 | { 17 | _options = options.ToList(); 18 | _standardEvasions = GetStandardEvasions(); 19 | } 20 | 21 | private List GetStandardEvasions() 22 | { 23 | return 24 | [ 25 | new WebDriver(), 26 | new ChromeSci(), 27 | new ChromeRuntime(), 28 | new Codec(), 29 | new Languages(GetOptionsByType()), 30 | new OutDimensions(), 31 | new Permissions(), 32 | new UserAgent(), 33 | new Vendor(GetOptionsByType()), 34 | new WebGl(GetOptionsByType()), 35 | new PluginEvasion(), 36 | new StackTrace(), 37 | new HardwareConcurrency(GetOptionsByType()), 38 | new ContentWindow(), 39 | new SourceUrl() 40 | ]; 41 | } 42 | 43 | protected internal override ICollection GetDependencies() 44 | { 45 | return _standardEvasions; 46 | } 47 | 48 | protected internal override async Task OnPageCreatedAsync(IPage page) 49 | { 50 | var utilsScript = StealthUtils.GetScript("Utils.js"); 51 | await page.EvaluateExpressionOnNewDocumentAsync(utilsScript); 52 | } 53 | 54 | public void AddOptions(T options) 55 | where T: IPuppeteerExtraPluginOptions 56 | { 57 | if (_options.OfType().Any()) 58 | { 59 | throw new ArgumentException("Option already exists", nameof(options)); 60 | } 61 | 62 | _options.Add(options); 63 | } 64 | 65 | private T GetOptionsByType() where T : IPuppeteerExtraPluginOptions 66 | { 67 | return _options.OfType().SingleOrDefault(); 68 | } 69 | 70 | public void RemoveEvasion() where T : PuppeteerExtraPlugin 71 | { 72 | _standardEvasions.RemoveAll(ev => ev is T); 73 | } 74 | 75 | public void RemoveEvasion(string name) 76 | { 77 | _standardEvasions.RemoveAll(ev => ev.Name == name); 78 | } 79 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/CapSolver/CapSolverApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using PuppeteerExtraSharp.Plugins.Recaptcha.ApiClient; 7 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider.CapSolver.Models; 8 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.CapSolver; 9 | 10 | internal class CapSolverApi(string userKey, CaptchaProviderOptions options) 11 | { 12 | private readonly ApiClient.ApiClient _client = new("https://api.capsolver.com"); 13 | 14 | public async Task CreateTaskAsync(GetRecaptchaSolutionRequest request) 15 | { 16 | var json = new Dictionary 17 | { 18 | ["clientKey"] = userKey, 19 | ["task"] = new Dictionary 20 | { 21 | ["websiteKey"] = request.SiteKey, 22 | ["websiteURL"] = request.PageUrl, 23 | ["type"] = request.Version switch 24 | { 25 | CaptchaVersion.V2 => "ReCaptchaV2TaskProxyless", 26 | CaptchaVersion.V3 => "ReCaptchaV3TaskProxyless", 27 | CaptchaVersion.HCaptcha => throw new NotSupportedException("HCaptcha is not supported"), 28 | _ => throw new ArgumentOutOfRangeException() 29 | }, 30 | ["isInvisible"] = request.IsInvisible, 31 | ["recaptchaDataSValue"] = request.DataS, 32 | } 33 | }; 34 | 35 | var cancellationToken = GetCancellationToken(); 36 | var result = await _client.PostAsync("createTask", json, cancellationToken); 37 | 38 | ThrowErrorIfBadStatus(result.ErrorId); 39 | return result; 40 | } 41 | 42 | public async Task GetSolution(string id) 43 | { 44 | var query = new Dictionary() 45 | { 46 | ["clientKey"] = userKey, 47 | ["taskId"] = id, 48 | }; 49 | 50 | var cancellationToken = GetCancellationToken(); 51 | var result = await _client.CreatePostPollingRequest("getTaskResult", query) 52 | .TriesLimit(options.MaxPollingAttempts) 53 | .ActivatePollingAsync(response => 54 | response.Data.Status == "processing" && response.Data.ErrorId == 0 55 | ? PollingAction.ContinuePolling 56 | : PollingAction.Break, 57 | cancellationToken); 58 | 59 | ThrowErrorIfBadStatus(result.Data.ErrorId, result.Data.ErrorDescription); 60 | return result.Data; 61 | } 62 | 63 | private void ThrowErrorIfBadStatus(int errorId, string? errorDescription = null) 64 | { 65 | if (errorId != 0) 66 | throw new HttpRequestException( 67 | $"CapSolver request ends with error id [{errorId} {errorDescription ?? string.Empty}]"); 68 | } 69 | 70 | private CancellationToken GetCancellationToken() 71 | { 72 | var source = new CancellationTokenSource(); 73 | source.CancelAfter(options.ApiTimeout); 74 | 75 | return source.Token; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/TwoCaptcha/TwoCaptchaApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using PuppeteerExtraSharp.Plugins.Recaptcha.ApiClient; 7 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider.TwoCaptcha.Models; 8 | 9 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.TwoCaptcha; 10 | 11 | internal class TwoCaptchaApi(string userKey, CaptchaProviderOptions options) 12 | { 13 | private readonly ApiClient.ApiClient _client = new("https://api.2captcha.com"); 14 | 15 | public async Task CreateTaskAsync(GetRecaptchaSolutionRequest request) 16 | { 17 | var json = new Dictionary 18 | { 19 | ["clientKey"] = userKey, 20 | ["task"] = new Dictionary() 21 | { 22 | ["websiteKey"] = request.SiteKey, 23 | ["websiteURL"] = request.PageUrl, 24 | ["type"] = request.Version switch 25 | { 26 | CaptchaVersion.V2 => "RecaptchaV2TaskProxyless ", 27 | CaptchaVersion.V3 => "RecaptchaV3TaskProxyless", 28 | CaptchaVersion.HCaptcha => throw new NotSupportedException("HCaptcha is not supported"), 29 | _ => throw new ArgumentOutOfRangeException() 30 | }, 31 | ["isInvisible"] = request.IsInvisible.ToString(), 32 | ["recaptchaDataSValue"] = request.DataS, 33 | ["minScore"] = request.MinV3RecaptchaScore.ToString() 34 | } 35 | }; 36 | 37 | var cancellationToken = GetCancellationToken(); 38 | var result = await _client.PostAsync("createTask", json, cancellationToken); 39 | 40 | ThrowErrorIfBadStatus(result.ErrorId); 41 | return result; 42 | } 43 | 44 | 45 | public async Task GetSolution(ulong id) 46 | { 47 | var query = new Dictionary() 48 | { 49 | ["clientKey"] = userKey, 50 | ["taskId"] = id.ToString(), 51 | }; 52 | 53 | var cancellationToken = GetCancellationToken(); 54 | var result = await _client.CreatePostPollingRequest("getTaskResult", query) 55 | .TriesLimit(options.MaxPollingAttempts) 56 | .ActivatePollingAsync(response => 57 | response.Data.Status == "processing" && response.Data.ErrorId == 0 58 | ? PollingAction.ContinuePolling 59 | : PollingAction.Break, 60 | cancellationToken); 61 | 62 | ThrowErrorIfBadStatus(result.Data.ErrorId, result.Data.ErrorDescription); 63 | return result.Data; 64 | } 65 | 66 | private void ThrowErrorIfBadStatus(int errorId, string? errorDescription = null) 67 | { 68 | if (errorId != 0) 69 | throw new HttpRequestException( 70 | $"Two captcha request ends with error id [{errorId} {errorDescription ?? string.Empty}]"); 71 | } 72 | 73 | private CancellationToken GetCancellationToken() 74 | { 75 | var source = new CancellationTokenSource(); 76 | source.CancelAfter(options.ApiTimeout); 77 | 78 | return source.Token; 79 | } 80 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/README.md: -------------------------------------------------------------------------------- 1 | ## Block Resources Plugin 2 | 3 | Block unwanted network requests (scripts, images, documents, etc.) using simple, composable rules. Useful for speeding up crawls, reducing bandwidth, and cutting out ads/trackers. 4 | 5 | #### Features 6 | - Rule-based blocking by: 7 | - Page instance 8 | - URL substring 9 | - Resource type (PuppeteerSharp ResourceType) 10 | - Custom predicate (IRequest => bool) 11 | - Works with PuppeteerExtra’s plugin pipeline 12 | - Configurable abort error code 13 | 14 | #### Quick start 15 | 16 | ```csharp 17 | // Create the plugin and register it 18 | var blockResourcesPlugin = new BlockResourcesPlugin(); 19 | var puppeteerExtra = new PuppeteerExtra(); 20 | 21 | var browser = await puppeteerExtra.Use(blockResourcesPlugin).LaunchAsync(); 22 | var page = await browser.NewPageAsync(); 23 | 24 | // Add some rules BEFORE navigation for best effect 25 | 26 | // 1) Block scripts for a specific page and URL pattern 27 | blockResourcesPlugin.AddRule(s => 28 | s.Page(page) 29 | .Url("ads.js") 30 | .Resources(ResourceType.Script)); 31 | 32 | // 2) Block non-navigation requests via a custom predicate 33 | blockResourcesPlugin.AddRule(s => 34 | s.Custom(request => !request.IsNavigationRequest)); 35 | 36 | // 3) Block all scripts on all pages 37 | blockResourcesPlugin.AddRule(s => 38 | s.Resources(ResourceType.Script)); 39 | 40 | // 4) Block all images on a specific page 41 | blockResourcesPlugin.AddRule(s => 42 | s.Page(page) 43 | .Resources(ResourceType.Image)); 44 | 45 | await page.GoToAsync("https://adblock.turtlecute.org/"); // many/most requests will be blocked 46 | ``` 47 | 48 | #### Rule builder reference 49 | Use AddRule with a builder to combine conditions: 50 | 51 | - Page(Page page) 52 | - Apply the rule only to requests originating from a specific PuppeteerSharp Page. 53 | - Url(string substring) 54 | - Apply the rule to requests whose URL contains the given substring. 55 | - Resources(ResourceType type) 56 | - Apply the rule to requests of a given resource type (e.g., Script, Image, Document, etc.). 57 | - Custom(Func predicate) 58 | - Apply the rule to requests that satisfy a custom predicate. 59 | 60 | Notes: 61 | - Chain multiple conditions in a single rule to narrow targeting (e.g., Page + Url + Resources). 62 | - Add multiple rules to cover different scenarios. 63 | 64 | #### Customize abort error code 65 | By default, blocked requests are aborted with RequestAbortErrorCode.BlockedByClient. You can override this: 66 | 67 | ```csharp 68 | var blockResourcesPlugin = new BlockResourcesPlugin(RequestAbortErrorCode.BlockedByClient); 69 | // ... register and use as usual 70 | ``` 71 | 72 | Pick the error code that best fits your use case (see PuppeteerSharp’s RequestAbortErrorCode). 73 | 74 | #### Custom rule class 75 | If you need full control, implement IBlockRule: 76 | 77 | ```csharp 78 | public class MyRule : IBlockRule 79 | { 80 | public bool ShouldBlock(IRequest request) 81 | { 82 | return request.IsNavigationRequest && request.Url.Contains("ads.js"); 83 | } 84 | } 85 | 86 | // Register your custom rule 87 | var blockResourcesPlugin = new BlockResourcesPlugin(); 88 | blockResourcesPlugin.AddRule(new MyRule()); 89 | ``` 90 | 91 | #### Tips 92 | - Add rules before navigating to ensure they apply from the first request. 93 | - Be careful when blocking navigation requests; it can prevent pages from loading. -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/readme.md: -------------------------------------------------------------------------------- 1 | ## reCAPTCHA Plugin 2 | 3 | Solve reCAPTCHA challenges programmatically with a single call. Supports reCAPTCHA v2, v3, and invisible (button/callback). 4 | 5 | ### Features 6 | - Single-call solve: detect the widget, request a token via the provider API, and inject it into the page. 7 | - Supports: 8 | - reCAPTCHA v2 (checkbox) 9 | - reCAPTCHA v2 Invisible (button/callback-triggered) 10 | - reCAPTCHA v3 (score-based) 11 | - Options for viewport-only solving, inactive/lazy widgets, debugging, timeouts, and v3 score threshold. 12 | - Pluggable provider design. Currently supported: [2Captcha](https://2captcha.com/?from=1937404), [CapSolver](https://www.capsolver.com/). 13 | 14 | ## Quick start 15 | 16 | ```csharp 17 | // Initialize the 2Captcha provider with your API key 18 | var twoCaptchaProvider = new TwoCaptcha(""); 19 | var recaptchaPlugin = new RecaptchaPlugin(twoCaptchaProvider); 20 | 21 | var puppeteerExtra = new PuppeteerExtra(); 22 | 23 | // Launch browser with the reCAPTCHA plugin enabled 24 | var browser = await puppeteerExtra.Use(recaptchaPlugin).LaunchAsync(); 25 | 26 | var page = await browser.NewPageAsync(); 27 | await page.GoToAsync("https://www.google.com/recaptcha/api2/demo"); 28 | 29 | // Single call to detect the widget, request a solution via the API, and inject the token 30 | await recaptchaPlugin.SolveCaptchaAsync(page); 31 | 32 | // Submit the form on the page 33 | var submitButton = await page.QuerySelectorAsync("#recaptcha-demo-submit"); 34 | await submitButton.ClickAsync(); 35 | ``` 36 | 37 | #### Advanced configuration 38 | 39 | ```csharp 40 | // Configure the 2Captcha provider (timeouts and polling behavior) 41 | var twoCaptchaProviderOptions = new CaptchaProviderOptions 42 | { 43 | ApiTimeout = TimeSpan.FromMinutes(2), // Maximum total time to wait for a solution 44 | MaxPollingAttempts = 5, // Max number of status checks before giving up 45 | StartTimeout = TimeSpan.FromSeconds(50), // Initial delay before polling (provider-side job start) 46 | }; 47 | 48 | var twoCaptchaProvider = new TwoCaptcha("", twoCaptchaProviderOptions); 49 | 50 | // Configure how the plugin detects and solves challenges 51 | var pluginOptions = new RecaptchaSolveOptions 52 | { 53 | ThrowOnError = true, // Throw exceptions on failure (otherwise return a soft failure) 54 | SolveInViewportOnly = true, // Only solve widgets visible in the viewport 55 | SolveScoreBased = true, // Enable handling of reCAPTCHA v3 (score-based) 56 | SolveInactiveChallenges = true, // Attempt to solve lazy/inactive widgets 57 | CaptchaWaitTimeout = TimeSpan.FromSeconds(10), // Wait time for a widget/challenge to appear 58 | Debug = false, // Verbose debug logging 59 | MinV3RecaptchaScore = 0.3, // Minimum acceptable v3 score 60 | SolveInvisibleChallenges = true, // Handle invisible/triggered reCAPTCHA 61 | }; 62 | 63 | var recaptchaPlugin = new RecaptchaPlugin(twoCaptchaProvider, pluginOptions); 64 | ``` 65 | 66 | #### Notes 67 | - reCAPTCHA v3 is score-based; adjust MinV3RecaptchaScore to match your tolerance. 68 | - For slow pages or delayed widgets, consider increasing CaptchaWaitTimeout and provider timeouts. 69 | - Set Debug = true to enable verbose diagnostics. 70 | 71 | #### Legal and ethical use 72 | Use this library only on properties you own or where you have explicit permission to automate. Respect target site terms of service. “reCAPTCHA” is a trademark of Google. 73 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Vendors/Google/GoogleVendor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 5 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Helpers; 6 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 7 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 8 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers; 9 | using PuppeteerSharp; 10 | 11 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Vendors.Google; 12 | 13 | public class GoogleVendor(ICaptchaSolverProvider provider, CaptchaOptions options) : ICaptchaVendor 14 | { 15 | public CaptchaVendor Vendor => CaptchaVendor.Google; 16 | 17 | public async Task WaitForCaptchasAsync(IPage page, TimeSpan timeout) 18 | { 19 | var handle = await page.QuerySelectorAsync( 20 | "script[src*=\"/recaptcha/api.js\"], script[src*=\"/recaptcha/enterprise.js\"]"); 21 | var hasRecaptchaScriptTag = handle != null; 22 | 23 | 24 | if (!hasRecaptchaScriptTag) return false; 25 | 26 | try 27 | { 28 | await page.WaitForFunctionAsync( 29 | "() => Object.keys((window.___grecaptcha_cfg || {}).clients || {}).length > 0", 30 | options: new WaitForFunctionOptions 31 | { 32 | PollingInterval = 500, 33 | Timeout = timeout.Milliseconds, 34 | }); 35 | return true; 36 | } 37 | catch 38 | { 39 | return false; 40 | } 41 | } 42 | 43 | public async Task FindCaptchasAsync(IPage page) 44 | { 45 | await LoadScriptAsync(page); 46 | return await page.EvaluateExpressionAsync("window.reScript.findRecaptchas()"); 47 | } 48 | 49 | public async Task> SolveCaptchasAsync(IPage page, ICollection captchas) 50 | { 51 | var solutions = new List(); 52 | foreach (var captcha in captchas) 53 | { 54 | var payload = await provider.GetSolutionAsync(new GetCaptchaSolutionRequest 55 | { 56 | Action = captcha.Action, 57 | DataS = captcha.S, 58 | IsEnterprise = captcha.IsEnterprise, 59 | IsInvisible = captcha.IsInvisible, 60 | PageUrl = captcha.Url, 61 | SiteKey = captcha.Sitekey, 62 | Version = captcha.CaptchaType == CaptchaType.score 63 | ? CaptchaVersion.RecaptchaV3 64 | : CaptchaVersion.RecaptchaV2, 65 | MinScore = options.MinScore, 66 | Vendor = CaptchaVendor.Google 67 | }); 68 | 69 | solutions.Add(new CaptchaSolution 70 | { 71 | Id = captcha.Id, 72 | Vendor = CaptchaVendor.Google, 73 | Payload = payload, 74 | }); 75 | } 76 | 77 | return solutions; 78 | } 79 | 80 | public async Task EnterCaptchaSolutionsAsync(IPage page, 81 | ICollection solutions) 82 | { 83 | await LoadScriptAsync(page); 84 | var result = await page.EvaluateFunctionAsync( 85 | @"(solutions) => {return window.reScript.enterRecaptchaSolutions(solutions)}", 86 | solutions); 87 | 88 | if (result is null) 89 | { 90 | throw new NullReferenceException("EnterCaptchaSolutionsAsync failed, result is null"); 91 | } 92 | 93 | return result; 94 | } 95 | 96 | public Task HandleOnPageCreatedAsync(IPage page) => Task.CompletedTask; 97 | 98 | public void ProcessResponseAsync(IPage page, object send, ResponseCreatedEventArgs e) 99 | { 100 | } 101 | 102 | private Task LoadScriptAsync(IPage page) 103 | { 104 | return page.EnsureEvaluateFunctionAsync( 105 | $"{GetType().Namespace}.{nameof(CaptchaVendor.Google)}Script.js", options); 106 | } 107 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/ContentWindow.js: -------------------------------------------------------------------------------- 1 | () => { 2 | try { 3 | // Adds a contentWindow proxy to the provided iframe element 4 | const addContentWindowProxy = iframe => { 5 | const contentWindowProxy = { 6 | get(target, key) { 7 | // Now to the interesting part: 8 | // We actually make this thing behave like a regular iframe window, 9 | // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :) 10 | // That makes it possible for these assertions to be correct: 11 | // iframe.contentWindow.self === window.top // must be false 12 | if (key === 'self') { 13 | return this 14 | } 15 | // iframe.contentWindow.frameElement === iframe // must be true 16 | if (key === 'frameElement') { 17 | return iframe 18 | } 19 | return Reflect.get(target, key) 20 | } 21 | } 22 | 23 | if (!iframe.contentWindow) { 24 | const proxy = new Proxy(window, contentWindowProxy) 25 | Object.defineProperty(iframe, 'contentWindow', { 26 | get() { 27 | return proxy 28 | }, 29 | set(newValue) { 30 | return newValue // contentWindow is immutable 31 | }, 32 | enumerable: true, 33 | configurable: false 34 | }) 35 | } 36 | } 37 | 38 | // Handles iframe element creation, augments `srcdoc` property so we can intercept further 39 | const handleIframeCreation = (target, thisArg, args) => { 40 | const iframe = target.apply(thisArg, args) 41 | 42 | // We need to keep the originals around 43 | const _iframe = iframe 44 | const _srcdoc = _iframe.srcdoc 45 | 46 | // Add hook for the srcdoc property 47 | // We need to be very surgical here to not break other iframes by accident 48 | Object.defineProperty(iframe, 'srcdoc', { 49 | configurable: true, // Important, so we can reset this later 50 | get: function () { 51 | return _iframe.srcdoc 52 | }, 53 | set: function (newValue) { 54 | addContentWindowProxy(this) 55 | // Reset property, the hook is only needed once 56 | Object.defineProperty(iframe, 'srcdoc', { 57 | configurable: false, 58 | writable: false, 59 | value: _srcdoc 60 | }) 61 | _iframe.srcdoc = newValue 62 | } 63 | }) 64 | return iframe 65 | } 66 | 67 | // Adds a hook to intercept iframe creation events 68 | const addIframeCreationSniffer = () => { 69 | /* global document */ 70 | const createElementHandler = { 71 | // Make toString() native 72 | get(target, key) { 73 | return Reflect.get(target, key) 74 | }, 75 | apply: function (target, thisArg, args) { 76 | const isIframe = 77 | args && args.length && `${args[0]}`.toLowerCase() === 'iframe' 78 | if (!isIframe) { 79 | // Everything as usual 80 | return target.apply(thisArg, args) 81 | } else { 82 | return handleIframeCreation(target, thisArg, args) 83 | } 84 | } 85 | } 86 | // All this just due to iframes with srcdoc bug 87 | utils.replaceWithProxy( 88 | document, 89 | 'createElement', 90 | createElementHandler 91 | ) 92 | } 93 | 94 | // Let's go 95 | addIframeCreationSniffer() 96 | } catch (err) { 97 | // console.warn(err) 98 | } 99 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Vendors/Cloudflare/CloudflareVendor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 5 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Helpers; 6 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 7 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 8 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers; 9 | using PuppeteerSharp; 10 | 11 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Vendors.Cloudflare; 12 | 13 | public class CloudflareVendor(ICaptchaSolverProvider provider, CaptchaOptions options) : ICaptchaVendor 14 | { 15 | public CaptchaVendor Vendor => CaptchaVendor.Cloudflare; 16 | 17 | public async Task WaitForCaptchasAsync(IPage page, TimeSpan timeout) 18 | { 19 | var handle = 20 | await page.QuerySelectorAsync( 21 | "script[src*=\"challenges.cloudflare.com/turnstile\"], script[src*=\"/turnstile/v0/api.js\"]"); 22 | 23 | var hasRecaptchaScriptTag = handle != null; 24 | 25 | if (!hasRecaptchaScriptTag) return false; 26 | 27 | const string selector = "div.cf-turnstile[data-sitekey], input[name='cf-turnstile-response']"; 28 | 29 | try 30 | { 31 | var exist = await page.WaitForSelectorAsync( 32 | selector, 33 | new WaitForSelectorOptions 34 | { 35 | Timeout = (int)timeout.TotalMilliseconds 36 | }); 37 | 38 | return exist != null; 39 | } 40 | catch 41 | { 42 | return false; 43 | } 44 | } 45 | 46 | public async Task FindCaptchasAsync(IPage page) 47 | { 48 | await LoadScriptAsync(page); 49 | return await page.EvaluateExpressionAsync("window.cfScript.findCaptchas()"); 50 | } 51 | 52 | public async Task> SolveCaptchasAsync(IPage page, ICollection captchas) 53 | { 54 | var solutions = new List(); 55 | foreach (var captcha in captchas) 56 | { 57 | var payload = await provider.GetSolutionAsync(new GetCaptchaSolutionRequest 58 | { 59 | Action = captcha.Action, 60 | DataS = captcha.S, 61 | IsEnterprise = captcha.IsEnterprise, 62 | IsInvisible = captcha.IsInvisible, 63 | PageUrl = captcha.Url, 64 | SiteKey = captcha.Sitekey, 65 | Version = captcha.CaptchaType == CaptchaType.score 66 | ? CaptchaVersion.RecaptchaV3 67 | : CaptchaVersion.RecaptchaV2, 68 | MinScore = options.MinScore, 69 | Vendor = CaptchaVendor.Cloudflare 70 | }); 71 | 72 | solutions.Add(new CaptchaSolution 73 | { 74 | Id = captcha.Id, 75 | Vendor = CaptchaVendor.Cloudflare, 76 | Payload = payload, 77 | }); 78 | } 79 | 80 | return solutions; 81 | } 82 | 83 | public async Task EnterCaptchaSolutionsAsync(IPage page, 84 | ICollection solutions) 85 | { 86 | await LoadScriptAsync(page); 87 | var result = await page.EvaluateFunctionAsync( 88 | @"(solutions) => {return window.cfScript.enterCaptchaSolutions(solutions)}", 89 | solutions); 90 | 91 | if (result is null) 92 | { 93 | throw new NullReferenceException("EnterCaptchaSolutionsAsync failed, result is null"); 94 | } 95 | 96 | return result; 97 | } 98 | 99 | public Task HandleOnPageCreatedAsync(IPage page) => Task.CompletedTask; 100 | 101 | public void ProcessResponseAsync(IPage page, object send, ResponseCreatedEventArgs e) 102 | { 103 | } 104 | 105 | private Task LoadScriptAsync(IPage page) 106 | { 107 | return page.EnsureEvaluateFunctionAsync( 108 | $"{GetType().Namespace}.{nameof(CaptchaVendor.Cloudflare)}Script.js", options); 109 | } 110 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Vendors/HCaptcha/HCaptchaVendor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 5 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Helpers; 6 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 7 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 8 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Providers; 9 | using PuppeteerSharp; 10 | 11 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Vendors.HCaptcha; 12 | 13 | public class HCaptchaVendor(ICaptchaSolverProvider provider, CaptchaOptions options) : ICaptchaVendor 14 | { 15 | public CaptchaVendor Vendor => CaptchaVendor.HCaptcha; 16 | 17 | public async Task WaitForCaptchasAsync(IPage page, TimeSpan timeout) 18 | { 19 | var handle = 20 | await page.QuerySelectorAsync( 21 | "script[src*=\"js.hcaptcha.com/1/api.js\"], script[src*=\"hcaptcha.com/1/api.js\"]"); 22 | var hasRecaptchaScriptTag = handle != null; 23 | 24 | if (!hasRecaptchaScriptTag) return false; 25 | 26 | const string selector = 27 | "iframe[src*='assets.hcaptcha.com/captcha/v1/'], " + 28 | "iframe[src*='newassets.hcaptcha.com/captcha/v1/']"; 29 | try 30 | { 31 | var exist = await page.WaitForSelectorAsync( 32 | selector, 33 | new WaitForSelectorOptions 34 | { 35 | Timeout = (int)timeout.TotalMilliseconds 36 | }); 37 | 38 | return exist != null; 39 | } 40 | catch 41 | { 42 | return false; 43 | } 44 | } 45 | 46 | public async Task FindCaptchasAsync(IPage page) 47 | { 48 | await LoadScriptAsync(page); 49 | return await page.EvaluateExpressionAsync("window.hcaptchaScript.findCaptchas()"); 50 | } 51 | 52 | public async Task> SolveCaptchasAsync(IPage page, ICollection captchas) 53 | { 54 | throw new NotSupportedException( 55 | $"hCaptcha solving support temporarily disabled."); 56 | var solutions = new List(); 57 | foreach (var captcha in captchas) 58 | { 59 | var payload = await provider.GetSolutionAsync(new GetCaptchaSolutionRequest 60 | { 61 | Action = captcha.Action, 62 | DataS = captcha.S, 63 | IsEnterprise = captcha.IsEnterprise, 64 | IsInvisible = captcha.IsInvisible, 65 | PageUrl = captcha.Url, 66 | SiteKey = captcha.Sitekey, 67 | Version = captcha.CaptchaType == CaptchaType.score 68 | ? CaptchaVersion.RecaptchaV3 69 | : CaptchaVersion.RecaptchaV2, 70 | MinScore = options.MinScore, 71 | Vendor = CaptchaVendor.HCaptcha 72 | }); 73 | 74 | solutions.Add(new CaptchaSolution 75 | { 76 | Id = captcha.Id, 77 | Vendor = CaptchaVendor.HCaptcha, 78 | Payload = payload, 79 | }); 80 | } 81 | 82 | return solutions; 83 | } 84 | 85 | public async Task EnterCaptchaSolutionsAsync(IPage page, 86 | ICollection solutions) 87 | { 88 | await LoadScriptAsync(page); 89 | var result = await page.EvaluateFunctionAsync( 90 | @"(solutions) => {return window.hcaptchaScript.enterCaptchaSolutions(solutions)}", 91 | solutions); 92 | 93 | if (result is null) 94 | { 95 | throw new NullReferenceException("EnterCaptchaSolutionsAsync failed, result is null"); 96 | } 97 | 98 | return result; 99 | } 100 | 101 | public Task HandleOnPageCreatedAsync(IPage page) => Task.CompletedTask; 102 | 103 | public void ProcessResponseAsync(IPage page, object send, ResponseCreatedEventArgs e) 104 | { 105 | } 106 | 107 | private Task LoadScriptAsync(IPage page) 108 | { 109 | return page.EnsureEvaluateFunctionAsync( 110 | $"{GetType().Namespace}.{nameof(CaptchaVendor.HCaptcha)}Script.js", options); 111 | } 112 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/PuppeteerExtraSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | latest 5 | 3.1.0 6 | https://github.com/Overmiind/Puppeteer-sharp-extra 7 | git 8 | puppeteer-extra recaptcha browser-automation browser-extension browser puppeteer netcore netcore80 stealth-client browser-testing c# twocaptcha anticaptcha block-resources adblock dotnet undetectable-browser 9 | https://github.com/Overmiind/Puppeteer-sharp-extra 10 | MIT 11 | 945328fb-3e7e-4518-99f8-ec578bf688b1 12 | README.md 13 | net8.0 14 | PuppeteerExtraSharp 15 | Overmind 16 | 17 | - Upgraded to PuppeteerSharp 20.0. This may require code updates due to upstream API changes. 18 | - Block Resources plugin fully rewritten with a clearer rule builder and more predictable behavior. 19 | - reCAPTCHA plugin fully rewritten with improved detection and support for v2, v3, and invisible challenges, plus new configuration options. 20 | - Stealth plugin stability and evasion fixes aligned with recent Chromium changes. 21 | - Dropped dependency on RestSharp; HTTP calls now use built-in primitives. 22 | - General code cleanup, modernization, and consistency improvements. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /Tests/CaptchaSolverTests/GoogleTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.CaptchaSolver; 3 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Interfaces; 4 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 5 | using PuppeteerSharp; 6 | using Xunit; 7 | 8 | namespace Extra.Tests.CaptchaSolverTests; 9 | 10 | public class GoogleTests : CaptchaSolverTestsBase 11 | { 12 | [Theory] 13 | [MemberData(nameof(Providers))] 14 | public async Task ShouldSolveCheckbox(ICaptchaSolverProvider provider) 15 | { 16 | var plugin = new CaptchaSolverPlugin(provider); 17 | var page = await LaunchAndGetPageAsync(plugin); 18 | 19 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox.php"); 20 | 21 | var result = await plugin.SolveCaptchaAsync(page, new CaptchaOptions 22 | { 23 | SolveInViewportOnly = true, 24 | SolveScoreBased = false, 25 | }); 26 | 27 | Assert.Null(result.Error); 28 | Assert.NotEmpty(result.Solved); 29 | Assert.NotEmpty(result.Filtered); 30 | Assert.All(result.Filtered, captcha => Assert.True(captcha.Captcha.IsInvisible)); 31 | 32 | await page.ClickAsync("button.form-field[type='submit']"); 33 | await Task.Delay(1000); 34 | var answerElement = 35 | await page.EvaluateExpressionAsync( 36 | "document.querySelector(\"body > main > h2:nth-child(3)\").textContent"); 37 | 38 | Assert.Equal("Success!", answerElement); 39 | } 40 | 41 | [Theory] 42 | [MemberData(nameof(Providers))] 43 | public async Task ShouldSolveInvisible(ICaptchaSolverProvider provider) 44 | { 45 | var plugin = new CaptchaSolverPlugin(provider); 46 | var page = await LaunchAndGetPageAsync(plugin); 47 | 48 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php", new NavigationOptions() 49 | { 50 | WaitUntil = 51 | [ 52 | WaitUntilNavigation.Networkidle0 53 | ] 54 | }); 55 | 56 | var result = await plugin.SolveCaptchaAsync(page, new CaptchaOptions 57 | { 58 | SolveInvisibleChallenges = true, 59 | SolveScoreBased = false, 60 | }); 61 | 62 | Assert.Null(result.Error); 63 | Assert.NotEmpty(result.Filtered); 64 | Assert.NotEmpty(result.Solved); 65 | Assert.All(result.Filtered, captcha => Assert.Equal(CaptchaType.score, captcha.Captcha.CaptchaType)); 66 | 67 | await Task.Delay(1000); 68 | var answerElement = 69 | await page.EvaluateExpressionAsync( 70 | "document.querySelector(\"body > main > h2:nth-child(3)\").textContent"); 71 | 72 | Assert.Equal("Success!", answerElement); 73 | } 74 | 75 | [Theory] 76 | [MemberData(nameof(Providers))] 77 | public async Task ShouldtSolveInvisible(ICaptchaSolverProvider provider) 78 | { 79 | var plugin = new CaptchaSolverPlugin(provider); 80 | var page = await LaunchAndGetPageAsync(plugin); 81 | 82 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php"); 83 | 84 | var result = await plugin.SolveCaptchaAsync(page, new CaptchaOptions 85 | { 86 | SolveInvisibleChallenges = false, 87 | SolveScoreBased = false, 88 | }); 89 | 90 | Assert.Empty(result.Solved); 91 | Assert.NotEmpty(result.Filtered); 92 | } 93 | 94 | [Theory] 95 | [MemberData(nameof(Providers))] 96 | public async Task ShouldSolveV3Captcha(ICaptchaSolverProvider provider) 97 | { 98 | var plugin = new CaptchaSolverPlugin(provider); 99 | var page = await LaunchAndGetPageAsync(plugin); 100 | 101 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php"); 102 | 103 | var result = await plugin.SolveCaptchaAsync(page); 104 | 105 | Assert.NotEmpty(result.Solved); 106 | Assert.Empty(result.Filtered); 107 | } 108 | 109 | [Theory] 110 | [MemberData(nameof(Providers))] 111 | public async Task ShouldntSolveWhenNoCaptcha(ICaptchaSolverProvider provider) 112 | { 113 | var plugin = new CaptchaSolverPlugin(provider); 114 | var page = await LaunchAndGetPageAsync(plugin); 115 | 116 | await page.GoToAsync("https://google.com"); 117 | 118 | var result = await plugin.SolveCaptchaAsync(page); 119 | 120 | Assert.Empty(result.Solved); 121 | Assert.Empty(result.Filtered); 122 | } 123 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/.gitignore: -------------------------------------------------------------------------------- 1 | # The following command works for downloading when using Git for Windows: 2 | # curl -LOf http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore 3 | # 4 | # Download this file using PowerShell v3 under Windows with the following comand: 5 | # Invoke-WebRequest https://gist.githubusercontent.com/kmorcinek/2710267/raw/ -OutFile .gitignore 6 | # 7 | # or wget: 8 | # wget --no-check-certificate http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.sln.docstates 14 | /tests/Resources.resx 15 | # Build results 16 | [Dd]ebug/ 17 | [Rr]elease/ 18 | x64/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | # build folder is nowadays used for build scripts and should not be ignored 22 | #build/ 23 | 24 | # NuGet Packages 25 | *.nupkg 26 | # The packages folder can be ignored because of Package Restore 27 | **/packages/* 28 | # except build/, which is used as an MSBuild target. 29 | !**/packages/build/ 30 | # Uncomment if necessary however generally it will be regenerated when needed 31 | #!**/packages/repositories.config 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | *_i.c 38 | *_p.c 39 | *.ilk 40 | *.meta 41 | *.obj 42 | *.pch 43 | *.pdb 44 | *.pgc 45 | *.pgd 46 | *.rsp 47 | *.sbr 48 | *.tlb 49 | *.tli 50 | *.tlh 51 | *.tmp 52 | *.tmp_proj 53 | *.log 54 | *.vspscc 55 | *.vssscc 56 | .builds 57 | *.pidb 58 | *.log 59 | *.scc 60 | *.resx 61 | # OS generated files # 62 | .DS_Store* 63 | Icon? 64 | 65 | # Visual C++ cache files 66 | ipch/ 67 | *.aps 68 | *.ncb 69 | *.opensdf 70 | *.sdf 71 | *.cachefile 72 | 73 | # Visual Studio profiler 74 | *.psess 75 | *.vsp 76 | *.vspx 77 | 78 | # Guidance Automation Toolkit 79 | *.gpState 80 | 81 | # ReSharper is a .NET coding add-in 82 | _ReSharper*/ 83 | *.[Rr]e[Ss]harper 84 | 85 | # TeamCity is a build add-in 86 | _TeamCity* 87 | 88 | # DotCover is a Code Coverage Tool 89 | *.dotCover 90 | 91 | # NCrunch 92 | *.ncrunch* 93 | .*crunch*.local.xml 94 | 95 | # Installshield output folder 96 | [Ee]xpress/ 97 | 98 | # DocProject is a documentation generator add-in 99 | DocProject/buildhelp/ 100 | DocProject/Help/*.HxT 101 | DocProject/Help/*.HxC 102 | DocProject/Help/*.hhc 103 | DocProject/Help/*.hhk 104 | DocProject/Help/*.hhp 105 | DocProject/Help/Html2 106 | DocProject/Help/html 107 | 108 | # Click-Once directory 109 | publish/ 110 | 111 | # Publish Web Output 112 | *.Publish.xml 113 | 114 | # Windows Azure Build Output 115 | csx 116 | *.build.csdef 117 | 118 | # Windows Store app package directory 119 | AppPackages/ 120 | 121 | # Others 122 | *.Cache 123 | ClientBin/ 124 | [Ss]tyle[Cc]op.* 125 | ~$* 126 | *~ 127 | *.dbmdl 128 | *.[Pp]ublish.xml 129 | *.pfx 130 | *.publishsettings 131 | modulesbin/ 132 | tempbin/ 133 | 134 | # EPiServer Site file (VPP) 135 | AppData/ 136 | 137 | # RIA/Silverlight projects 138 | Generated_Code/ 139 | 140 | # Backup & report files from converting an old project file to a newer 141 | # Visual Studio version. Backup files are not needed, because we have git ;-) 142 | _UpgradeReport_Files/ 143 | Backup*/ 144 | UpgradeLog*.XML 145 | UpgradeLog*.htm 146 | 147 | # vim 148 | *.txt~ 149 | *.swp 150 | *.swo 151 | 152 | # Temp files when opening LibreOffice on ubuntu 153 | .~lock.* 154 | 155 | # svn 156 | .svn 157 | 158 | # CVS - Source Control 159 | **/CVS/ 160 | 161 | # Remainings from resolving conflicts in Source Control 162 | *.orig 163 | 164 | # SQL Server files 165 | **/App_Data/*.mdf 166 | **/App_Data/*.ldf 167 | **/App_Data/*.sdf 168 | 169 | 170 | #LightSwitch generated files 171 | GeneratedArtifacts/ 172 | _Pvt_Extensions/ 173 | ModelManifest.xml 174 | 175 | # ========================= 176 | # Windows detritus 177 | # ========================= 178 | 179 | # Windows image file caches 180 | Thumbs.db 181 | ehthumbs.db 182 | 183 | # Folder config file 184 | Desktop.ini 185 | 186 | # Recycle Bin used on file shares 187 | $RECYCLE.BIN/ 188 | 189 | # Mac desktop service store files 190 | .DS_Store 191 | 192 | # SASS Compiler cache 193 | .sass-cache 194 | 195 | # Visual Studio 2014 CTP 196 | **/*.sln.ide 197 | 198 | # Visual Studio temp something 199 | .vs/ 200 | 201 | # dotnet stuff 202 | project.lock.json 203 | 204 | # VS 2015+ 205 | *.vc.vc.opendb 206 | *.vc.db 207 | 208 | # Rider 209 | .idea/ 210 | 211 | # Visual Studio Code 212 | .vscode/ 213 | 214 | # Output folder used by Webpack or other FE stuff 215 | **/node_modules/* 216 | **/wwwroot/* 217 | 218 | # SpecFlow specific 219 | *.feature.cs 220 | *.feature.xlsx.* 221 | *.Specs_*.html 222 | 223 | ##### 224 | # End of core ignore list, below put you custom 'per project' settings (patterns or path) 225 | ##### -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/CaptchaSolver/Models/CaptchaOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PuppeteerExtraSharp.Plugins.CaptchaSolver.Enums; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.CaptchaSolver.Models; 6 | 7 | public class CaptchaOptions 8 | { 9 | private static readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromSeconds(5); 10 | private const double DefaultMinScore = 0.5; 11 | private double _minScore = DefaultMinScore; 12 | 13 | private static readonly HashSet DefaultEnabledVendors = new() 14 | { 15 | CaptchaVendor.Google, 16 | CaptchaVendor.Cloudflare, 17 | CaptchaVendor.GeeTest, 18 | CaptchaVendor.DataDome, 19 | 20 | // hCaptcha support temporarily disabled. 21 | // 22 | // Reason: 23 | // hCaptcha has adopted an extremely aggressive anti–captcha-solver policy, 24 | // sending cease-and-desist letters to major solving services and breaking most 25 | // automated solving methods. As a result, the majority of public solvers have 26 | // shut down or become unreliable. 27 | // 28 | // Detection still works, but solving is intentionally not attempted. 29 | // 30 | // We will reconsider hCaptcha support only if a reliable and compliant solution 31 | // becomes available in the future. 32 | CaptchaVendor.HCaptcha, 33 | }; 34 | 35 | public HashSet EnabledVendors { get; set; } = DefaultEnabledVendors; 36 | 37 | /// 38 | /// Maximum time to wait for captcha to appear/solve. Default: 10 seconds. 39 | /// 40 | public TimeSpan CaptchaWaitTimeout { get; set; } = DefaultWaitTimeout; 41 | 42 | /// 43 | /// Throw on errors instead of returning them in the error property. 44 | /// 45 | public bool ThrowOnError { get; set; } = false; 46 | 47 | /// 48 | /// Minimal acceptable score for captcha based on score (range 0..1). Default: 0.5. 49 | /// 50 | public double MinScore 51 | { 52 | get => _minScore; 53 | set 54 | { 55 | if (value < 0 || value > 1) 56 | throw new ArgumentOutOfRangeException(nameof(MinScore), "Value must be in range [0, 1]."); 57 | _minScore = value; 58 | } 59 | } 60 | 61 | /// 62 | /// Only solve captchas and challenges visible in the viewport. 63 | /// 64 | public bool SolveInViewportOnly { get; set; } = false; 65 | 66 | /// 67 | /// Solve invisible captchas used to acquire a score and not present a challenge (e.g. reCAPTCHA v3). 68 | /// 69 | public bool SolveScoreBased { get; set; } = true; 70 | 71 | /// 72 | /// Solve invisible captchas that have no active challenge. 73 | /// 74 | public bool SolveInactiveChallenges { get; set; } = true; 75 | 76 | /// 77 | /// Solve invisible challenges (checkbox not shown) when present. 78 | /// 79 | public bool SolveInvisibleChallenges { get; set; } = true; 80 | 81 | /// 82 | /// Enable verbose debug logging. 83 | /// 84 | public bool Debug { get; set; } = false; 85 | 86 | /// 87 | /// Proxy type (http, https, socks4, socks5). Required for DataDome. 88 | /// 89 | public string? ProxyType { get; set; } 90 | 91 | /// 92 | /// Proxy address (IP or hostname). Required for DataDome. 93 | /// 94 | public string? ProxyAddress { get; set; } 95 | 96 | /// 97 | /// Proxy port. Required for DataDome. 98 | /// 99 | public int? ProxyPort { get; set; } 100 | 101 | /// 102 | /// Proxy authentication username. 103 | /// 104 | public string? ProxyLogin { get; set; } 105 | 106 | /// 107 | /// Proxy authentication password. 108 | /// 109 | public string? ProxyPassword { get; set; } 110 | 111 | /// 112 | /// Session ID for sticky proxy sessions. 113 | /// Use this to ensure the same IP is used across requests (required for DataDome). 114 | /// For DataImpulse proxies, this is appended as ";sessid.{value}" to the username. 115 | /// Example: "mysession123" will result in "user;sessid.mysession123" as the proxy username. 116 | /// 117 | public string? ProxySessionId { get; set; } 118 | 119 | /// 120 | /// Returns true if proxy settings are configured. 121 | /// 122 | public bool HasProxy => !string.IsNullOrEmpty(ProxyAddress) && ProxyPort.HasValue; 123 | } -------------------------------------------------------------------------------- /Tests/Recaptcha/RecaptchaTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Properties; 4 | using PuppeteerExtraSharp.Plugins.Recaptcha; 5 | using PuppeteerExtraSharp.Plugins.Recaptcha.Models; 6 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 7 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider.TwoCaptcha; 8 | using Xunit; 9 | 10 | namespace Extra.Tests.Recaptcha; 11 | 12 | public class RecaptchaTests : BrowserDefault 13 | { 14 | [Fact] 15 | public async Task ShouldSolveInvisible() 16 | { 17 | var plugin = new RecaptchaPlugin(new TwoCaptcha(Resources.TwoCaptchaKey, new CaptchaProviderOptions() 18 | { 19 | StartTimeout = TimeSpan.FromSeconds(30), MaxPollingAttempts = 20, ApiTimeout = TimeSpan.FromMinutes(3), 20 | })); 21 | var page = await LaunchAndGetPageAsync(plugin); 22 | 23 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php"); 24 | 25 | var result = await plugin.SolveCaptchaAsync(page, new RecaptchaSolveOptions()); 26 | 27 | Assert.Null(result.Error); 28 | Assert.NotEmpty(result.Solved); 29 | 30 | await Task.Delay(3000); 31 | var answerElement = 32 | await page.EvaluateExpressionAsync( 33 | "document.querySelector(\"body > main > h2:nth-child(3)\").textContent"); 34 | 35 | Assert.Equal("Success!", answerElement); 36 | } 37 | 38 | [Fact] 39 | public async Task ShouldSolveCheckbox() 40 | { 41 | var plugin = new RecaptchaPlugin(new TwoCaptcha(Resources.TwoCaptchaKey, new CaptchaProviderOptions() 42 | { 43 | StartTimeout = TimeSpan.FromSeconds(30), MaxPollingAttempts = 20, ApiTimeout = TimeSpan.FromMinutes(3), 44 | })); 45 | var page = await LaunchAndGetPageAsync(plugin); 46 | 47 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox.php"); 48 | 49 | var result = await plugin.SolveCaptchaAsync(page, new RecaptchaSolveOptions 50 | { 51 | SolveInViewportOnly = true, 52 | SolveScoreBased = false, 53 | }); 54 | 55 | Assert.Null(result.Error); 56 | Assert.NotEmpty(result.Solved); 57 | Assert.NotEmpty(result.Filtered); 58 | Assert.All(result.Filtered, captcha => Assert.True(captcha.Captcha.IsInvisible)); 59 | 60 | await page.ClickAsync("button.form-field[type='submit']"); 61 | await Task.Delay(2000); 62 | var answerElement = 63 | await page.EvaluateExpressionAsync( 64 | "document.querySelector(\"body > main > h2:nth-child(3)\").textContent"); 65 | 66 | Assert.Equal("Success!", answerElement); 67 | } 68 | 69 | [Fact] 70 | public async Task ShouldtSolveInvisible() 71 | { 72 | var plugin = new RecaptchaPlugin(new TwoCaptcha(Resources.TwoCaptchaKey, new CaptchaProviderOptions() 73 | { 74 | StartTimeout = TimeSpan.FromSeconds(30), MaxPollingAttempts = 20, ApiTimeout = TimeSpan.FromMinutes(3), 75 | })); 76 | var page = await LaunchAndGetPageAsync(plugin); 77 | 78 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php"); 79 | 80 | var result = await plugin.SolveCaptchaAsync(page, new RecaptchaSolveOptions 81 | { 82 | SolveInvisibleChallenges = false, 83 | SolveScoreBased = false, 84 | }); 85 | 86 | Assert.Empty(result.Solved); 87 | Assert.NotEmpty(result.Filtered); 88 | } 89 | 90 | [Fact] 91 | public async Task ShouldSolveV3Captcha() 92 | { 93 | var plugin = new RecaptchaPlugin(new TwoCaptcha(Resources.TwoCaptchaKey, new CaptchaProviderOptions() 94 | { 95 | StartTimeout = TimeSpan.FromSeconds(30), MaxPollingAttempts = 20, ApiTimeout = TimeSpan.FromMinutes(3), 96 | })); 97 | var page = await LaunchAndGetPageAsync(plugin); 98 | 99 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php"); 100 | 101 | var result = await plugin.SolveCaptchaAsync(page); 102 | 103 | Assert.NotEmpty(result.Solved); 104 | Assert.Empty(result.Filtered); 105 | } 106 | 107 | [Fact] 108 | public async Task ShouldntSolveWhenNoCaptcha() 109 | { 110 | var plugin = new RecaptchaPlugin(new TwoCaptcha(Resources.TwoCaptchaKey)); 111 | var page = await LaunchAndGetPageAsync(plugin); 112 | 113 | await page.GoToAsync("https://google.com"); 114 | 115 | var result = await plugin.SolveCaptchaAsync(page); 116 | 117 | Assert.Empty(result.Solved); 118 | Assert.Empty(result.Filtered); 119 | } 120 | } -------------------------------------------------------------------------------- /Tests/Recaptcha/CapSolverRecaptchaTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Properties; 4 | using PuppeteerExtraSharp.Plugins.Recaptcha; 5 | using PuppeteerExtraSharp.Plugins.Recaptcha.Models; 6 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 7 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider.CapSolver; 8 | using Xunit; 9 | 10 | namespace Extra.Tests.Recaptcha; 11 | 12 | public class CapSolverRecaptchaTests : BrowserDefault 13 | { 14 | [Fact] 15 | public async Task ShouldSolveInvisible() 16 | { 17 | var plugin = new RecaptchaPlugin(new CapSolver(Resources.CapSolverKey, new CaptchaProviderOptions() 18 | { 19 | StartTimeout = TimeSpan.FromSeconds(10), MaxPollingAttempts = 30, ApiTimeout = TimeSpan.FromMinutes(3), 20 | })); 21 | var page = await LaunchAndGetPageAsync(plugin); 22 | 23 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php"); 24 | 25 | var result = await plugin.SolveCaptchaAsync(page, new RecaptchaSolveOptions()); 26 | 27 | Assert.Null(result.Error); 28 | Assert.NotEmpty(result.Solved); 29 | 30 | await Task.Delay(3000); 31 | var answerElement = 32 | await page.EvaluateExpressionAsync( 33 | "document.querySelector(\"body > main > h2:nth-child(3)\").textContent"); 34 | 35 | Assert.Equal("Success!", answerElement); 36 | } 37 | 38 | [Fact] 39 | public async Task ShouldSolveCheckbox() 40 | { 41 | var plugin = new RecaptchaPlugin(new CapSolver(Resources.CapSolverKey, new CaptchaProviderOptions() 42 | { 43 | StartTimeout = TimeSpan.FromSeconds(10), MaxPollingAttempts = 30, ApiTimeout = TimeSpan.FromMinutes(3), 44 | })); 45 | var page = await LaunchAndGetPageAsync(plugin); 46 | 47 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox.php"); 48 | 49 | var result = await plugin.SolveCaptchaAsync(page, new RecaptchaSolveOptions 50 | { 51 | SolveInViewportOnly = true, 52 | SolveScoreBased = false, 53 | }); 54 | 55 | Assert.Null(result.Error); 56 | Assert.NotEmpty(result.Solved); 57 | Assert.NotEmpty(result.Filtered); 58 | Assert.All(result.Filtered, captcha => Assert.True(captcha.Captcha.IsInvisible)); 59 | 60 | await page.ClickAsync("button.form-field[type='submit']"); 61 | await Task.Delay(2000); 62 | var answerElement = 63 | await page.EvaluateExpressionAsync( 64 | "document.querySelector(\"body > main > h2:nth-child(3)\").textContent"); 65 | 66 | Assert.Equal("Success!", answerElement); 67 | } 68 | 69 | [Fact] 70 | public async Task ShouldtSolveInvisible() 71 | { 72 | var plugin = new RecaptchaPlugin(new CapSolver(Resources.CapSolverKey, new CaptchaProviderOptions() 73 | { 74 | StartTimeout = TimeSpan.FromSeconds(10), MaxPollingAttempts = 30, ApiTimeout = TimeSpan.FromMinutes(3), 75 | })); 76 | var page = await LaunchAndGetPageAsync(plugin); 77 | 78 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php"); 79 | 80 | var result = await plugin.SolveCaptchaAsync(page, new RecaptchaSolveOptions 81 | { 82 | SolveInvisibleChallenges = false, 83 | SolveScoreBased = false, 84 | }); 85 | 86 | Assert.Empty(result.Solved); 87 | Assert.NotEmpty(result.Filtered); 88 | } 89 | 90 | [Fact] 91 | public async Task ShouldSolveV3Captcha() 92 | { 93 | var plugin = new RecaptchaPlugin(new CapSolver(Resources.CapSolverKey, new CaptchaProviderOptions() 94 | { 95 | StartTimeout = TimeSpan.FromSeconds(10), MaxPollingAttempts = 30, ApiTimeout = TimeSpan.FromMinutes(3), 96 | })); 97 | var page = await LaunchAndGetPageAsync(plugin); 98 | 99 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php"); 100 | 101 | var result = await plugin.SolveCaptchaAsync(page); 102 | 103 | Assert.NotEmpty(result.Solved); 104 | Assert.Empty(result.Filtered); 105 | } 106 | 107 | [Fact] 108 | public async Task ShouldntSolveWhenNoCaptcha() 109 | { 110 | var plugin = new RecaptchaPlugin(new CapSolver(Resources.CapSolverKey)); 111 | var page = await LaunchAndGetPageAsync(plugin); 112 | 113 | await page.GoToAsync("https://google.com"); 114 | 115 | var result = await plugin.SolveCaptchaAsync(page); 116 | 117 | Assert.Empty(result.Solved); 118 | Assert.Empty(result.Filtered); 119 | } 120 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/LoadTimes.js: -------------------------------------------------------------------------------- 1 | () => { 2 | if (!window.chrome) { 3 | // Use the exact property descriptor found in headful Chrome 4 | // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')` 5 | Object.defineProperty(window, 'chrome', { 6 | writable: true, 7 | enumerable: true, 8 | configurable: false, // note! 9 | value: {} // We'll extend that later 10 | }) 11 | } 12 | 13 | // That means we're running headful and don't need to mock anything 14 | if ('loadTimes' in window.chrome) { 15 | return // Nothing to do here 16 | } 17 | 18 | // Check that the Navigation Timing API v1 + v2 is available, we need that 19 | if ( 20 | !window.performance || 21 | !window.performance.timing || 22 | !window.PerformancePaintTiming 23 | ) { 24 | return 25 | } 26 | 27 | const {performance} = window 28 | 29 | // Some stuff is not available on about:blank as it requires a navigation to occur, 30 | // let's harden the code to not fail then: 31 | const ntEntryFallback = { 32 | nextHopProtocol: 'h2', 33 | type: 'other' 34 | } 35 | 36 | // The API exposes some funky info regarding the connection 37 | const protocolInfo = { 38 | get connectionInfo() { 39 | const ntEntry = 40 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 41 | return ntEntry.nextHopProtocol 42 | }, 43 | get npnNegotiatedProtocol() { 44 | // NPN is deprecated in favor of ALPN, but this implementation returns the 45 | // HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN. 46 | const ntEntry = 47 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 48 | return ['h2', 'hq'].includes(ntEntry.nextHopProtocol) 49 | ? ntEntry.nextHopProtocol 50 | : 'unknown' 51 | }, 52 | get navigationType() { 53 | const ntEntry = 54 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 55 | return ntEntry.type 56 | }, 57 | get wasAlternateProtocolAvailable() { 58 | // The Alternate-Protocol header is deprecated in favor of Alt-Svc 59 | // (https://www.mnot.net/blog/2016/03/09/alt-svc), so technically this 60 | // should always return false. 61 | return false 62 | }, 63 | get wasFetchedViaSpdy() { 64 | // SPDY is deprecated in favor of HTTP/2, but this implementation returns 65 | // true for HTTP/2 or HTTP2+QUIC/39 as well. 66 | const ntEntry = 67 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 68 | return ['h2', 'hq'].includes(ntEntry.nextHopProtocol) 69 | }, 70 | get wasNpnNegotiated() { 71 | // NPN is deprecated in favor of ALPN, but this implementation returns true 72 | // for HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN. 73 | const ntEntry = 74 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 75 | return ['h2', 'hq'].includes(ntEntry.nextHopProtocol) 76 | } 77 | } 78 | 79 | const {timing} = window.performance 80 | 81 | // Truncate number to specific number of decimals, most of the `loadTimes` stuff has 3 82 | function toFixed(num, fixed) { 83 | var re = new RegExp('^-?\\d+(?:.\\d{0,' + (fixed || -1) + '})?') 84 | return num.toString().match(re)[0] 85 | } 86 | 87 | const timingInfo = { 88 | get firstPaintAfterLoadTime() { 89 | // This was never actually implemented and always returns 0. 90 | return 0 91 | }, 92 | get requestTime() { 93 | return timing.navigationStart / 1000 94 | }, 95 | get startLoadTime() { 96 | return timing.navigationStart / 1000 97 | }, 98 | get commitLoadTime() { 99 | return timing.responseStart / 1000 100 | }, 101 | get finishDocumentLoadTime() { 102 | return timing.domContentLoadedEventEnd / 1000 103 | }, 104 | get finishLoadTime() { 105 | return timing.loadEventEnd / 1000 106 | }, 107 | get firstPaintTime() { 108 | const fpEntry = performance.getEntriesByType('paint')[0] || { 109 | startTime: timing.loadEventEnd / 1000 // Fallback if no navigation occured (`about:blank`) 110 | } 111 | return toFixed( 112 | (fpEntry.startTime + performance.timeOrigin) / 1000, 113 | 3 114 | ) 115 | } 116 | } 117 | 118 | window.chrome.loadTimes = function () { 119 | return { 120 | ...protocolInfo, 121 | ...timingInfo 122 | } 123 | } 124 | utils.patchToString(window.chrome.loadTimes) 125 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/PuppeteerExtra.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using PuppeteerExtraSharp.Plugins; 6 | using PuppeteerSharp; 7 | 8 | 9 | namespace PuppeteerExtraSharp; 10 | 11 | public class PuppeteerExtra 12 | { 13 | private List _plugins = []; 14 | 15 | public PuppeteerExtra Use(PuppeteerExtraPlugin plugin) 16 | { 17 | _plugins.Add(plugin); 18 | ResolveDependencies(plugin); 19 | plugin.OnPluginRegisteredAsync(); 20 | return this; 21 | } 22 | 23 | public Task LaunchAsync() 24 | { 25 | return LaunchAsync(new LaunchOptions()); 26 | } 27 | 28 | public async Task LaunchAsync(LaunchOptions options) 29 | { 30 | _plugins.ForEach(e => e.BeforeLaunchAsync(options)); 31 | var browser = await Puppeteer.LaunchAsync(options); 32 | _plugins.ForEach(e => e.AfterLaunchAsync(browser)); 33 | await OnStartAsync(new BrowserStartContext 34 | { 35 | StartType = StartType.Launch, 36 | IsHeadless = options.Headless 37 | }, browser); 38 | return browser; 39 | } 40 | 41 | public async Task ConnectAsync(ConnectOptions options) 42 | { 43 | _plugins.ForEach(e => e.BeforeConnectAsync(options)); 44 | var browser = await Puppeteer.ConnectAsync(options); 45 | _plugins.ForEach(e => e.AfterConnectAsync(browser)); 46 | await OnStartAsync(new BrowserStartContext 47 | { 48 | StartType = StartType.Connect 49 | }, browser); 50 | return browser; 51 | } 52 | 53 | public T GetPlugin() where T : PuppeteerExtraPlugin 54 | { 55 | return (T)_plugins.FirstOrDefault(e => e.GetType() == typeof(T)); 56 | } 57 | 58 | private async Task OnStartAsync(BrowserStartContext context, IBrowser browser) 59 | { 60 | OrderPlugins(); 61 | CheckPluginRequirements(context); 62 | await RegisterAsync(browser); 63 | } 64 | 65 | private void ResolveDependencies(PuppeteerExtraPlugin plugin) 66 | { 67 | var dependencies = plugin.GetDependencies()?.ToList(); 68 | if (dependencies is null || !dependencies.Any()) 69 | return; 70 | 71 | foreach (var puppeteerExtraPlugin in dependencies) 72 | { 73 | Use(puppeteerExtraPlugin); 74 | 75 | var plugDependencies = puppeteerExtraPlugin.GetDependencies()?.ToList(); 76 | 77 | if (plugDependencies != null && plugDependencies.Any()) 78 | plugDependencies.ForEach(ResolveDependencies); 79 | } 80 | } 81 | 82 | private void OrderPlugins() 83 | { 84 | _plugins = _plugins.OrderBy(e => e.Requirements?.Contains(PluginRequirements.RunLast)).ToList(); 85 | } 86 | 87 | private void CheckPluginRequirements(BrowserStartContext context) 88 | { 89 | foreach (var puppeteerExtraPlugin in _plugins) 90 | { 91 | if (puppeteerExtraPlugin.Requirements is null) 92 | continue; 93 | 94 | foreach (var requirement in puppeteerExtraPlugin.Requirements) 95 | switch (context.StartType) 96 | { 97 | case StartType.Launch when requirement == PluginRequirements.HeadFul && context.IsHeadless: 98 | throw new NotSupportedException( 99 | $"Plugin - {puppeteerExtraPlugin.Name} is not supported in headless mode"); 100 | case StartType.Connect when requirement == PluginRequirements.Launch: 101 | throw new NotSupportedException( 102 | $"Plugin - {puppeteerExtraPlugin.Name} doesn't support connect"); 103 | } 104 | } 105 | } 106 | 107 | private async Task RegisterAsync(IBrowser browser) 108 | { 109 | var pages = await browser.PagesAsync(); 110 | 111 | browser.TargetCreated += async (_, args) => 112 | { 113 | _plugins.ForEach(e => e.OnTargetCreatedAsync(args.Target)); 114 | if (args.Target.Type == TargetType.Page) 115 | { 116 | var page = await args.Target.PageAsync(); 117 | _plugins.ForEach(async void (e) => await e.OnPageCreatedAsync(page)); 118 | } 119 | }; 120 | 121 | foreach (var puppeteerExtraPlugin in _plugins) 122 | { 123 | browser.TargetChanged += (sender, args) => puppeteerExtraPlugin.OnTargetChangedAsync(args.Target); 124 | browser.TargetDestroyed += (sender, args) => puppeteerExtraPlugin.OnTargetDestroyedAsync(args.Target); 125 | browser.Disconnected += (sender, args) => puppeteerExtraPlugin.OnDisconnectedAsync(); 126 | browser.Closed += (sender, args) => puppeteerExtraPlugin.OnCloseAsync(); 127 | foreach (var page in pages) await puppeteerExtraPlugin.OnPageCreatedAsync(page); 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/RecapchaPlugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using PuppeteerExtraSharp.Plugins.CaptchaSolver; 6 | using PuppeteerExtraSharp.Plugins.Recaptcha.Models; 7 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 8 | using PuppeteerSharp; 9 | 10 | namespace PuppeteerExtraSharp.Plugins.Recaptcha; 11 | 12 | [Obsolete($"Use {nameof(CaptchaSolverPlugin)} instead. This plugin will be removed in a future version.", UrlFormat = "https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/CaptchaSolver")] 13 | public class RecaptchaPlugin : PuppeteerExtraPlugin 14 | { 15 | private readonly ICaptchaHandler _handler; 16 | private readonly RecaptchaSolveOptions _defaultOptions; 17 | 18 | public RecaptchaPlugin( 19 | IRecaptchaProvider provider, 20 | RecaptchaSolveOptions options = null, 21 | ICaptchaHandler handler = null) : base("recaptcha") 22 | { 23 | _defaultOptions = options ?? new RecaptchaSolveOptions(); 24 | _handler = handler ?? new RecaptchaHandler(provider, _defaultOptions); 25 | } 26 | 27 | public async Task SolveCaptchaAsync(IPage page, 28 | RecaptchaSolveOptions optionsOverride = null) 29 | { 30 | var options = optionsOverride ?? _defaultOptions; 31 | 32 | var hasCaptchas = await _handler.WaitForCaptchasAsync(page, options.CaptchaWaitTimeout); 33 | 34 | if (!hasCaptchas) 35 | { 36 | return new EnterCaptchaSolutionsResult() 37 | { 38 | Error = "No captchas found" 39 | }; 40 | } 41 | 42 | var captchaResponse = await _handler.FindCaptchasAsync(page); 43 | 44 | if (options.ThrowOnError) 45 | { 46 | throw new CaptchaException(page.Url, captchaResponse.Error); 47 | } 48 | 49 | var filteredCaptchas = FilterCaptchas(captchaResponse.Captchas, options); 50 | 51 | var captchas = filteredCaptchas.unfiltered.ToList(); 52 | if (captchas.Count == 0) 53 | { 54 | return new EnterCaptchaSolutionsResult() 55 | { 56 | Error = "No captchas found or all captchas have been filtered", 57 | Filtered = filteredCaptchas.filtered, 58 | }; 59 | } 60 | 61 | var solvedCaptchas = await _handler.SolveCaptchasAsync(page, captchas); 62 | var result = await _handler.EnterCaptchaSolutionsAsync(page, solvedCaptchas); 63 | result.Filtered = filteredCaptchas.filtered; 64 | 65 | if (options.ThrowOnError && string.IsNullOrWhiteSpace(result.Error)) 66 | { 67 | throw new CaptchaException(page.Url, result.Error); 68 | } 69 | 70 | return result; 71 | } 72 | 73 | protected internal override async Task OnPageCreatedAsync(IPage page) 74 | { 75 | await page.SetBypassCSPAsync(true); 76 | } 77 | 78 | private (ICollection unfiltered, ICollection filtered) FilterCaptchas( 79 | ICollection captchas, 80 | RecaptchaSolveOptions options) 81 | { 82 | var filteredCaptchas = new List(); 83 | var unfilteredCaptchas = new List(); 84 | foreach (var captcha in captchas) 85 | { 86 | switch (captcha.CaptchaType) 87 | { 88 | case CaptchaType.invisible when !options.SolveInvisibleChallenges: 89 | filteredCaptchas.Add(new FilteredCaptcha() 90 | { 91 | Captcha = captcha, 92 | FilteredReason = "solveInvisibleChallenges" 93 | }); 94 | continue; 95 | case CaptchaType.invisible when 96 | !captcha.HasActiveChallengePopup && 97 | !options.SolveInactiveChallenges: 98 | filteredCaptchas.Add(new FilteredCaptcha() 99 | { 100 | Captcha = captcha, 101 | FilteredReason = "solveInactiveChallenges" 102 | }); 103 | continue; 104 | case CaptchaType.score when !options.SolveScoreBased: 105 | filteredCaptchas.Add(new FilteredCaptcha() 106 | { 107 | Captcha = captcha, 108 | FilteredReason = "solveScoreBased" 109 | }); 110 | continue; 111 | case CaptchaType.checkbox when !captcha.IsInViewport && options.SolveInViewportOnly: 112 | filteredCaptchas.Add(new FilteredCaptcha() 113 | { 114 | Captcha = captcha, 115 | FilteredReason = "solveInViewportOnly" 116 | }); 117 | continue; 118 | default: 119 | unfilteredCaptchas.Add(captcha); 120 | break; 121 | } 122 | } 123 | 124 | return (unfilteredCaptchas, filteredCaptchas); 125 | } 126 | } -------------------------------------------------------------------------------- /Tests/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | 10 | namespace Extra.Tests.Properties { 11 | using System; 12 | 13 | 14 | /// 15 | /// A strongly-typed resource class, for looking up localized strings, etc. 16 | /// 17 | // This class was auto-generated by the StronglyTypedResourceBuilder 18 | // class via a tool like ResGen or Visual Studio. 19 | // To add or remove a member, edit your .ResX file then rerun ResGen 20 | // with the /str option, or rebuild your VS project. 21 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 22 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 23 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 24 | internal class Resources { 25 | 26 | private static global::System.Resources.ResourceManager resourceMan; 27 | 28 | private static global::System.Globalization.CultureInfo resourceCulture; 29 | 30 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 31 | internal Resources() { 32 | } 33 | 34 | /// 35 | /// Returns the cached ResourceManager instance used by this class. 36 | /// 37 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 38 | internal static global::System.Resources.ResourceManager ResourceManager { 39 | get { 40 | if (object.ReferenceEquals(resourceMan, null)) { 41 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Extra.Tests.Properties.Resources", typeof(Resources).Assembly); 42 | resourceMan = temp; 43 | } 44 | return resourceMan; 45 | } 46 | } 47 | 48 | /// 49 | /// Overrides the current thread's CurrentUICulture property for all 50 | /// resource lookups using this strongly typed resource class. 51 | /// 52 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 53 | internal static global::System.Globalization.CultureInfo Culture { 54 | get { 55 | return resourceCulture; 56 | } 57 | set { 58 | resourceCulture = value; 59 | } 60 | } 61 | 62 | /// 63 | /// Looks up a localized string similar to . 64 | /// 65 | internal static string AntiCaptchaKey { 66 | get { 67 | return ResourceManager.GetString("AntiCaptchaKey", resourceCulture); 68 | } 69 | } 70 | 71 | /// 72 | /// Looks up a localized string similar to . 73 | /// 74 | internal static string CapSolverKey { 75 | get { 76 | return ResourceManager.GetString("CapSolverKey", resourceCulture); 77 | } 78 | } 79 | 80 | /// 81 | /// Looks up a localized string similar to . 82 | /// 83 | internal static string ProxyIp { 84 | get { 85 | return ResourceManager.GetString("ProxyIp", resourceCulture); 86 | } 87 | } 88 | 89 | /// 90 | /// Looks up a localized string similar to . 91 | /// 92 | internal static string ProxyLogin { 93 | get { 94 | return ResourceManager.GetString("ProxyLogin", resourceCulture); 95 | } 96 | } 97 | 98 | /// 99 | /// Looks up a localized string similar to . 100 | /// 101 | internal static string ProxyPassword { 102 | get { 103 | return ResourceManager.GetString("ProxyPassword", resourceCulture); 104 | } 105 | } 106 | 107 | /// 108 | /// Looks up a localized string similar to 0. 109 | /// 110 | internal static string ProxyPort { 111 | get { 112 | return ResourceManager.GetString("ProxyPort", resourceCulture); 113 | } 114 | } 115 | 116 | /// 117 | /// Looks up a localized string similar to . 118 | /// 119 | internal static string TwoCaptchaKey { 120 | get { 121 | return ResourceManager.GetString("TwoCaptchaKey", resourceCulture); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/RecaptchaHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using PuppeteerExtraSharp.Plugins.CaptchaSolver; 6 | using PuppeteerExtraSharp.Plugins.Recaptcha.Helpers; 7 | using PuppeteerExtraSharp.Plugins.Recaptcha.Models; 8 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 9 | using PuppeteerExtraSharp.Utils; 10 | using PuppeteerSharp; 11 | 12 | namespace PuppeteerExtraSharp.Plugins.Recaptcha; 13 | 14 | [Obsolete($"Use {nameof(CaptchaSolverPlugin)} instead. This plugin will be removed in a future version.", UrlFormat = "https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/CaptchaSolver")] 15 | public class RecaptchaHandler(IRecaptchaProvider provider, RecaptchaSolveOptions options) : ICaptchaHandler 16 | { 17 | private readonly RecaptchaSolveOptions _options = options ?? new RecaptchaSolveOptions(); 18 | public async Task WaitForCaptchasAsync(IPage page, TimeSpan timeout) 19 | { 20 | var handle = await page.QuerySelectorAsync( 21 | "script[src*=\"/recaptcha/api.js\"], script[src*=\"/recaptcha/enterprise.js\"]"); 22 | var hasRecaptchaScriptTag = handle != null; 23 | 24 | 25 | if (!hasRecaptchaScriptTag) return false; 26 | 27 | try 28 | { 29 | await page.WaitForFunctionAsync( 30 | "() => Object.keys((window.___grecaptcha_cfg || {}).clients || {}).length > 0", 31 | options: new WaitForFunctionOptions 32 | { 33 | PollingInterval = 500, 34 | // Use TotalMilliseconds to avoid truncating to 0-999 ms component 35 | Timeout = (int)timeout.TotalMilliseconds, 36 | }); 37 | return true; 38 | } 39 | catch 40 | { 41 | return false; 42 | } 43 | } 44 | 45 | public async Task FindCaptchasAsync(IPage page) 46 | { 47 | await LoadScriptAsync(page, _options); 48 | // Double-check presence of helper and attempt one more load if missing (e.g., after navigation) 49 | var hasScript = await page.EvaluateExpressionAsync("typeof window.reScript !== 'undefined'"); 50 | if (!hasScript) 51 | { 52 | await LoadScriptAsync(page, _options); 53 | } 54 | var captchaResponse = await page.EvaluateExpressionAsync("window.reScript.findRecaptchas()"); 55 | return captchaResponse; 56 | } 57 | 58 | public async Task> SolveCaptchasAsync(IPage page, ICollection captchas) 59 | { 60 | await LoadScriptAsync(page, _options); 61 | 62 | var solutions = new List(); 63 | foreach (var captcha in captchas) 64 | { 65 | var solution = await provider.GetSolutionAsync(new GetRecaptchaSolutionRequest() 66 | { 67 | Action = captcha.Action, 68 | DataS = captcha.S, 69 | IsEnterprise = captcha.IsEnterprise, 70 | IsInvisible = captcha.IsInvisible, 71 | PageUrl = captcha.Url, 72 | SiteKey = captcha.Sitekey, 73 | Version = captcha.CaptchaType == CaptchaType.score ? CaptchaVersion.V3 : CaptchaVersion.V2, 74 | MinV3RecaptchaScore = _options.MinV3RecaptchaScore, 75 | }); 76 | 77 | solutions.Add(new CaptchaSolution 78 | { 79 | Id = captcha.Id, 80 | Text = solution, 81 | }); 82 | } 83 | 84 | return solutions; 85 | } 86 | 87 | public async Task EnterCaptchaSolutionsAsync(IPage page, 88 | ICollection solutions) 89 | { 90 | // Ensure helper is available before invoking it 91 | await LoadScriptAsync(page, _options); 92 | var solutionArgs = solutions.Select(s => new 93 | { 94 | id = s.Id, 95 | text = s.Text, 96 | }); 97 | 98 | var result = await page.EvaluateFunctionAsync( 99 | @"(solutions) => {return window.reScript.enterRecaptchaSolutions(solutions)}", 100 | solutionArgs); 101 | 102 | if (result is null) 103 | { 104 | throw new NullReferenceException("EnterCaptchaSolutionsAsync failed, result is null"); 105 | } 106 | 107 | return result; 108 | } 109 | 110 | private async Task LoadScriptAsync(IPage page, params object[] args) 111 | { 112 | var recaptchaScriptName = GetType().Namespace + ".Scripts.RecaptchaScript.js"; 113 | var script = ResourcesReader.ReadFile(recaptchaScriptName); 114 | await page.EnsureEvaluateFunctionAsync(recaptchaScriptName, script, args); 115 | 116 | // Wait until the global helper is available to avoid race conditions 117 | try 118 | { 119 | await page.WaitForFunctionAsync( 120 | "() => typeof window.reScript !== 'undefined'", 121 | new WaitForFunctionOptions 122 | { 123 | PollingInterval = 100, 124 | Timeout = 5000, 125 | }); 126 | } 127 | catch 128 | { 129 | // If this fails, caller will attempt to reload. No throw here to stay resilient. 130 | } 131 | } 132 | } --------------------------------------------------------------------------------