├── .gitignore ├── LICENSE ├── PuppeteerExtraSharp ├── .gitignore ├── BrowserStartContext.cs ├── Plugins │ ├── AnonymizeUa │ │ └── AnonymizeUaPlugin.cs │ ├── BlockResources │ │ ├── BlockResourcesPlugin.cs │ │ ├── BlockRule.cs │ │ ├── README.md │ │ └── ResourcesBlockBuilder.cs │ ├── ExtraStealth │ │ ├── Evasions │ │ │ ├── ChromeApp.cs │ │ │ ├── ChromeRuntime.cs │ │ │ ├── ChromeSci.cs │ │ │ ├── Codec.cs │ │ │ ├── ContentWindow.cs │ │ │ ├── HardwareConcurrency.cs │ │ │ ├── Languages.cs │ │ │ ├── LoadTimes.cs │ │ │ ├── OutDimensions.cs │ │ │ ├── Permissions.cs │ │ │ ├── PluginEvasion.cs │ │ │ ├── SourceUrl.cs │ │ │ ├── StackTrace.cs │ │ │ ├── UserAgent.cs │ │ │ ├── Vendor.cs │ │ │ ├── WebDriver.cs │ │ │ └── WebGl.cs │ │ ├── Scripts │ │ │ ├── ChromeApp.js │ │ │ ├── Codec.js │ │ │ ├── ContentWindow.js │ │ │ ├── HardwareConcurrency.js │ │ │ ├── Language.js │ │ │ ├── LoadTimes.js │ │ │ ├── Outdimensions.js │ │ │ ├── Permissions.js │ │ │ ├── Plugin.js │ │ │ ├── Runtime.js │ │ │ ├── SCI.js │ │ │ ├── Stacktrace.js │ │ │ ├── Utils.js │ │ │ ├── Vendor.js │ │ │ ├── WebDriver.js │ │ │ └── WebGL.js │ │ ├── StealthPlugin.cs │ │ ├── Utils.cs │ │ └── readme.md │ ├── PuppeteerExtraPlugin.cs │ ├── PuppeteerExtraPluginOptions.cs │ └── Recaptcha │ │ ├── CaptchaCfg.cs │ │ ├── CaptchaException.cs │ │ ├── CaptchaOptions.cs │ │ ├── Provider │ │ ├── 2Captcha │ │ │ ├── Models │ │ │ │ ├── TwoCaptchaRequest.cs │ │ │ │ └── TwoCaptchaResponse.cs │ │ │ ├── TwoCaptcha.cs │ │ │ └── TwoCaptchaApi.cs │ │ ├── AntiCaptcha │ │ │ ├── AntiCaptcha.cs │ │ │ ├── AntiCaptchaApi.cs │ │ │ └── Models │ │ │ │ ├── AntiCaptchaRequest.cs │ │ │ │ └── TaskResultModel.cs │ │ ├── IRecaptchaProvider.cs │ │ └── ProviderOptions.cs │ │ ├── RecapchaPlugin.cs │ │ ├── Recaptcha.cs │ │ ├── RecaptchaResult.cs │ │ ├── RestClient │ │ ├── PollingBuilder.cs │ │ └── RestClient.cs │ │ ├── Scripts │ │ └── EnterRecaptchaCallBackScript.js │ │ └── readme.md ├── PuppeteerExtra.cs ├── PuppeteerExtraSharp.csproj ├── PuppeteerExtraSharp.sln ├── README.md └── Utils │ ├── ResoursesReader.cs │ └── RestHelper.cs ├── README.md └── Tests ├── BlockResourcesTests └── BlockResourcesPluginTests.cs ├── BrowserDefault.cs ├── Constants.cs ├── Extra.Tests.csproj ├── ExtraLaunchTest.cs ├── Properties ├── Resources.Designer.cs └── Resources.resx ├── Recaptcha ├── AntiCaptcha │ └── AntiCaptchaTests.cs ├── RestClientTest │ └── RestClientTests.cs └── TwoCaptcha │ └── TwoCaptchaProviderTest.cs ├── StealthPluginTests ├── EvasionsTests │ ├── ChromeAppTest.cs │ ├── ChromeSciTest.cs │ ├── CodecTest.cs │ ├── ContentWindowTest.cs │ ├── LanguagesTest.cs │ ├── LoadTimesTest.cs │ ├── PermissionsTest.cs │ ├── PluginEvasionTest.cs │ ├── PluginTest.cs │ ├── RuntimeTest.cs │ ├── SourceUrl │ │ ├── SourceUrlTest.cs │ │ └── fixtures │ │ │ └── Test.html │ ├── StealthPluginTest.cs │ ├── UserAgentTest.cs │ ├── VendorTest.cs │ └── WebDriverTest.cs ├── Script │ └── fpCollect.js └── StealthPluginTests.cs └── Utils └── FingerPrint.cs /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | Tests/Resources.resx 36 | Tests/Resources.Designer.cs 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # .NET Core 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | **/Properties/launchSettings.json 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | 78 | # Chutzpah Test files 79 | _Chutzpah* 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opendb 86 | *.opensdf 87 | *.sdf 88 | *.cachefile 89 | *.VC.db 90 | *.VC.VC.opendb 91 | 92 | # Visual Studio profiler 93 | *.psess 94 | *.vsp 95 | *.vspx 96 | *.sap 97 | 98 | # TFS 2012 Local Workspace 99 | $tf/ 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | *.DotSettings.user 108 | 109 | # JustCode is a .NET coding add-in 110 | .JustCode 111 | 112 | # TeamCity is a build add-in 113 | _TeamCity* 114 | 115 | # DotCover is a Code Coverage Tool 116 | *.dotCover 117 | 118 | # Visual Studio code coverage results 119 | *.coverage 120 | *.coveragexml 121 | 122 | # NCrunch 123 | _NCrunch_* 124 | .*crunch*.local.xml 125 | nCrunchTemp_* 126 | 127 | # MightyMoose 128 | *.mm.* 129 | AutoTest.Net/ 130 | 131 | # Web workbench (sass) 132 | .sass-cache/ 133 | 134 | # Installshield output folder 135 | [Ee]xpress/ 136 | 137 | # DocProject is a documentation generator add-in 138 | DocProject/buildhelp/ 139 | DocProject/Help/*.HxT 140 | DocProject/Help/*.HxC 141 | DocProject/Help/*.hhc 142 | DocProject/Help/*.hhk 143 | DocProject/Help/*.hhp 144 | DocProject/Help/Html2 145 | DocProject/Help/html 146 | 147 | # Click-Once directory 148 | publish/ 149 | 150 | # Publish Web Output 151 | *.[Pp]ublish.xml 152 | *.azurePubxml 153 | # TODO: Comment the next line if you want to checkin your web deploy settings 154 | # but database connection strings (with potential passwords) will be unencrypted 155 | *.pubxml 156 | *.publishproj 157 | 158 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 159 | # checkin your Azure Web App publish settings, but sensitive information contained 160 | # in these scripts will be unencrypted 161 | PublishScripts/ 162 | 163 | # NuGet Packages 164 | *.nupkg 165 | # The packages folder can be ignored because of Package Restore 166 | **/packages/* 167 | # except build/, which is used as an MSBuild target. 168 | !**/packages/build/ 169 | # Uncomment if necessary however generally it will be regenerated when needed 170 | #!**/packages/repositories.config 171 | # NuGet v3's project.json files produces more ignorable files 172 | *.nuget.props 173 | *.nuget.targets 174 | 175 | # Microsoft Azure Build Output 176 | csx/ 177 | *.build.csdef 178 | 179 | # Microsoft Azure Emulator 180 | ecf/ 181 | rcf/ 182 | 183 | # Windows Store app package directories and files 184 | AppPackages/ 185 | BundleArtifacts/ 186 | Package.StoreAssociation.xml 187 | _pkginfo.txt 188 | 189 | # Visual Studio cache files 190 | # files ending in .cache can be ignored 191 | *.[Cc]ache 192 | # but keep track of directories ending in .cache 193 | !*.[Cc]ache/ 194 | 195 | # Others 196 | ClientBin/ 197 | ~$* 198 | *~ 199 | *.dbmdl 200 | *.dbproj.schemaview 201 | *.jfm 202 | *.pfx 203 | *.publishsettings 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | *.ndf 225 | 226 | # Business Intelligence projects 227 | *.rdl.data 228 | *.bim.layout 229 | *.bim_*.settings 230 | 231 | # Microsoft Fakes 232 | FakesAssemblies/ 233 | 234 | # GhostDoc plugin setting file 235 | *.GhostDoc.xml 236 | 237 | # Node.js Tools for Visual Studio 238 | .ntvs_analysis.dat 239 | node_modules/ 240 | 241 | # Typescript v1 declaration files 242 | typings/ 243 | 244 | # Visual Studio 6 build log 245 | *.plg 246 | 247 | # Visual Studio 6 workspace options file 248 | *.opt 249 | 250 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 251 | *.vbw 252 | 253 | # Visual Studio LightSwitch build output 254 | **/*.HTMLClient/GeneratedArtifacts 255 | **/*.DesktopClient/GeneratedArtifacts 256 | **/*.DesktopClient/ModelManifest.xml 257 | **/*.Server/GeneratedArtifacts 258 | **/*.Server/ModelManifest.xml 259 | _Pvt_Extensions 260 | 261 | # Paket dependency manager 262 | .paket/paket.exe 263 | paket-files/ 264 | 265 | # FAKE - F# Make 266 | .fake/ 267 | 268 | # JetBrains Rider 269 | .idea/ 270 | *.sln.iml 271 | 272 | # CodeRush 273 | .cr/ 274 | 275 | # Python Tools for Visual Studio (PTVS) 276 | __pycache__/ 277 | *.pyc 278 | 279 | # Cake - Uncomment if you are using it 280 | # tools/** 281 | # !tools/packages.config 282 | 283 | # Telerik's JustMock configuration file 284 | *.jmconfig 285 | 286 | # BizTalk build output 287 | *.btp.cs 288 | *.btm.cs 289 | *.odx.cs 290 | *.xsd.cs 291 | Thumbs.db 292 | 293 | # ignore local cert 294 | /lib/PuppeteerSharp.TestServer/testCert.cer 295 | /lib/PuppeteerSharp.Tests/Screenshots/test.png 296 | 297 | demos/PuppeteerSharpPdfDemo/google.pdf 298 | demos/PuppeteerSharpPdfDemo/.local-chromium/ 299 | docs/ 300 | samples/PupppeterSharpAspNetFrameworkSample/PupppeterSharpAspNetFrameworkSample/App_Data/ 301 | /Tests/Resources.cs 302 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | } 14 | } 15 | -------------------------------------------------------------------------------- /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 9 | { 10 | public AnonymizeUaPlugin(): base("anonymize-ua") 11 | { 12 | } 13 | 14 | private Func _customAction; 15 | public void CustomizeUa(Func uaAction) 16 | { 17 | _customAction = uaAction; 18 | } 19 | 20 | public override async Task OnPageCreated(IPage page) 21 | { 22 | var ua = await page.Browser.GetUserAgentAsync(); 23 | ua = ua.Replace("HeadlessChrome", "Chrome"); 24 | 25 | var regex = new Regex(@"/\(([^)]+)\)/"); 26 | ua = regex.Replace(ua, "(Windows NT 10.0; Win64; x64)"); 27 | 28 | if (_customAction != null) 29 | ua = _customAction(ua); 30 | 31 | await page.SetUserAgentAsync(ua); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/BlockResourcesPlugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using PuppeteerSharp; 6 | 7 | namespace PuppeteerExtraSharp.Plugins.BlockResources 8 | { 9 | public class BlockResourcesPlugin : PuppeteerExtraPlugin 10 | { 11 | public readonly List BlockResources = new List(); 12 | 13 | public BlockResourcesPlugin(IEnumerable blockResources = null): base("block-resources") 14 | { 15 | if (blockResources != null) 16 | AddRule(builder => builder.BlockedResources(blockResources.ToArray())); 17 | } 18 | 19 | public BlockRule AddRule(Action builderAction) 20 | { 21 | var builder = new ResourcesBlockBuilder(); 22 | builderAction(builder); 23 | 24 | var rule = builder.Build(); 25 | this.BlockResources.Add(builder.Build()); 26 | 27 | return rule; 28 | } 29 | 30 | public BlockResourcesPlugin RemoveRule(BlockRule rule) 31 | { 32 | BlockResources.Remove(rule); 33 | return this; 34 | } 35 | 36 | 37 | public override async Task OnPageCreated(IPage page) 38 | { 39 | await page.SetRequestInterceptionAsync(true); 40 | page.Request += (sender, args) => OnPageRequest(page, args); 41 | 42 | } 43 | 44 | 45 | private async void OnPageRequest(IPage sender, RequestEventArgs e) 46 | { 47 | if (BlockResources.Any(rule => rule.IsRequestBlocked(sender, e.Request))) 48 | { 49 | await e.Request.AbortAsync(); 50 | return; 51 | } 52 | 53 | await e.Request.ContinueAsync(); 54 | } 55 | 56 | 57 | public override void BeforeLaunch(LaunchOptions options) 58 | { 59 | options.Args = options.Args.Append("--site-per-process").Append("--disable-features=IsolateOrigins").ToArray(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/BlockRule.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.BlockResources 6 | { 7 | public class BlockRule 8 | { 9 | public string SitePattern; 10 | public IPage IPage; 11 | public HashSet ResourceType = new HashSet(); 12 | 13 | internal BlockRule() 14 | { 15 | 16 | } 17 | 18 | public bool IsRequestBlocked(IPage fromPage, IRequest request) 19 | { 20 | if (!IsResourcesBlocked(request.ResourceType)) 21 | return false; 22 | 23 | return IsSiteBlocked(request.Url) || IsPageBlocked(fromPage); 24 | } 25 | 26 | 27 | public bool IsPageBlocked(IPage page) 28 | { 29 | return IPage != null && page.Equals(IPage); 30 | } 31 | 32 | public bool IsSiteBlocked(string siteUrl) 33 | { 34 | return !string.IsNullOrWhiteSpace(SitePattern) && Regex.IsMatch(siteUrl, SitePattern); 35 | } 36 | 37 | public bool IsResourcesBlocked(ResourceType resource) 38 | { 39 | return ResourceType.Contains(resource); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/README.md: -------------------------------------------------------------------------------- 1 | ## Block resoures plugin 2 | 3 | Blocks page resources in Puppeteer requests (img, documents etc.) 4 | 5 | ## Usage 6 | 7 | ### Add rule 8 | ```c# 9 | var plugin = new BlockResourcesPlugin(); 10 | var extra = new PuppeteerExtra().Use(plugin); 11 | var browser = await extra.LaunchAsync(new LaunchOptions()); 12 | var page = await browser.NewPageAsync(); 13 | 14 | // creates rule for resources blocking 15 | plugin.AddRule(builder => builder.BlockedResources(ResourceType.Image)); 16 | ``` 17 | ### Block resources by type 18 | 19 | ```c# 20 | // blocks images for all pages 21 | plugin.AddRule(builder => builder.BlockedResources(ResourceType.Image)); 22 | ``` 23 | 24 | ### Block resources by page 25 | 26 | ```c# 27 | // blocks images for current page 28 | plugin.AddRule(builder => builder.BlockedResources(ResourceType.Image).OnlyForPage(page)); 29 | ``` 30 | 31 | ### Block resources by request url 32 | 33 | ```c# 34 | // blocks images when request url equals pattern 35 | plugin.AddRule(builder => builder.BlockedResources(ResourceType.Image).ForUrl("msn")); 36 | ``` 37 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/BlockResources/ResourcesBlockBuilder.cs: -------------------------------------------------------------------------------- 1 | using PuppeteerSharp; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.BlockResources 4 | { 5 | public class ResourcesBlockBuilder 6 | { 7 | private BlockRule Rule { get; set; } = new BlockRule(); 8 | 9 | public ResourcesBlockBuilder BlockedResources(params ResourceType[] resources) 10 | { 11 | foreach (var resourceType in resources) 12 | { 13 | Rule.ResourceType.Add(resourceType); 14 | } 15 | 16 | return this; 17 | } 18 | 19 | public ResourcesBlockBuilder OnlyForPage(IPage page) 20 | { 21 | Rule.IPage = page; 22 | return this; 23 | } 24 | 25 | public ResourcesBlockBuilder ForUrl(string pattern) 26 | { 27 | Rule.SitePattern = pattern; 28 | return this; 29 | } 30 | 31 | internal BlockRule Build() 32 | { 33 | return this.Rule; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/ChromeApp.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Threading.Tasks; 3 | using PuppeteerSharp; 4 | [assembly: InternalsVisibleTo("Extra.Tests")] 5 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions 6 | { 7 | public class ChromeApp : PuppeteerExtraPlugin 8 | { 9 | public ChromeApp(): base("stealth-chromeApp") { } 10 | 11 | public override Task OnPageCreated(IPage page) 12 | { 13 | var script = Utils.GetScript("ChromeApp.js"); 14 | return Utils.EvaluateOnNewPage(page, script); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 7 | { 8 | public ChromeRuntime(): base("stealth-runtime") { } 9 | 10 | public override Task OnPageCreated(IPage page) 11 | { 12 | var script = Utils.GetScript("Runtime.js"); 13 | return Utils.EvaluateOnNewPage(page, script); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 7 | { 8 | public ChromeSci(): base("stealth_sci") { } 9 | 10 | public override Task OnPageCreated(IPage page) 11 | { 12 | var script = Utils.GetScript("SCI.js"); 13 | return Utils.EvaluateOnNewPage(page, script); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 7 | { 8 | public Codec() : base("stealth-codec") { } 9 | 10 | public override Task OnPageCreated(IPage page) 11 | { 12 | var script = Utils.GetScript("Codec.js"); 13 | return Utils.EvaluateOnNewPage(page, script); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 8 | { 9 | public ContentWindow() : base("Iframe.ContentWindow") { } 10 | 11 | public override List Requirements { get; set; } = new() 12 | { 13 | PluginRequirements.RunLast 14 | }; 15 | 16 | public override Task OnPageCreated(IPage page) 17 | { 18 | var script = Utils.GetScript("ContentWindow.js"); 19 | return Utils.EvaluateOnNewPage(page, script); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /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 : PuppeteerExtraPlugin 7 | { 8 | public StealthHardwareConcurrencyOptions Options { get; } 9 | 10 | public HardwareConcurrency(StealthHardwareConcurrencyOptions options = null) : base("stealth/hardwareConcurrency") 11 | { 12 | Options = options ?? new StealthHardwareConcurrencyOptions(4); 13 | } 14 | 15 | public override Task OnPageCreated(IPage page) 16 | { 17 | var script = Utils.GetScript("HardwareConcurrency.js"); 18 | return Utils.EvaluateOnNewPage(page, script, Options.Concurrency); 19 | } 20 | } 21 | 22 | public class StealthHardwareConcurrencyOptions : IPuppeteerExtraPluginOptions 23 | { 24 | public int Concurrency { get; } 25 | 26 | public StealthHardwareConcurrencyOptions(int concurrency) 27 | { 28 | Concurrency = concurrency; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 : PuppeteerExtraPlugin 8 | { 9 | public StealthLanguagesOptions Options { get; } 10 | 11 | public Languages(StealthLanguagesOptions options = null) : base("stealth-language") 12 | { 13 | Options = options ?? new StealthLanguagesOptions("en-US", "en"); 14 | } 15 | 16 | public override Task OnPageCreated(IPage page) 17 | { 18 | var script = Utils.GetScript("Language.js"); 19 | return Utils.EvaluateOnNewPage(page,script, Options.Languages); 20 | } 21 | } 22 | 23 | public class StealthLanguagesOptions : IPuppeteerExtraPluginOptions 24 | { 25 | public object[] Languages { get; } 26 | 27 | public StealthLanguagesOptions(params string[] languages) 28 | { 29 | Languages = languages.Cast().ToArray(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 7 | { 8 | public LoadTimes() : base("stealth-loadTimes") { } 9 | 10 | public override Task OnPageCreated(IPage page) 11 | { 12 | var script = Utils.GetScript("LoadTimes.js"); 13 | return Utils.EvaluateOnNewPage(page, script); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/OutDimensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions 6 | { 7 | public class OutDimensions : PuppeteerExtraPlugin 8 | { 9 | public OutDimensions() : base("stealth-dimensions") { } 10 | 11 | public override async Task OnPageCreated(IPage page) 12 | { 13 | var script = Utils.GetScript("Outdimensions.js"); 14 | await page.EvaluateFunctionOnNewDocumentAsync(script); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 7 | { 8 | public Permissions() : base("stealth-permissions") { } 9 | 10 | public override Task OnPageCreated(IPage page) 11 | { 12 | var script = Utils.GetScript("Permissions.js"); 13 | return Utils.EvaluateOnNewPage(page, script); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 7 | { 8 | public PluginEvasion() : base("stealth-pluginEvasion") 9 | { 10 | } 11 | 12 | public override async Task OnPageCreated(IPage page) 13 | { 14 | var scipt = Utils.GetScript("Plugin.js"); 15 | await Utils.EvaluateOnNewPage(page, scipt); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /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 8 | { 9 | public SourceUrl() : base("SourceUrl") 10 | { 11 | } 12 | 13 | public override async Task OnPageCreated(IPage page) 14 | { 15 | var mainWordProperty = 16 | page.MainFrame.GetType().GetProperty("MainWorld", BindingFlags.NonPublic 17 | | BindingFlags.Public | BindingFlags.Instance); 18 | var mainWordGetters = mainWordProperty.GetGetMethod(true); 19 | 20 | page.Load += async (_, _) => 21 | { 22 | var mainWord = mainWordGetters.Invoke(page.MainFrame, null); 23 | var contextField = mainWord.GetType() 24 | .GetField("_contextResolveTaskWrapper", BindingFlags.NonPublic | BindingFlags.Instance); 25 | if (contextField is not null) 26 | { 27 | var context = (TaskCompletionSource) contextField.GetValue(mainWord); 28 | var execution = await context.Task; 29 | var suffixField = execution.GetType() 30 | .GetField("_evaluationScriptSuffix", BindingFlags.NonPublic | BindingFlags.Instance); 31 | suffixField?.SetValue(execution, "//# sourceURL=''"); 32 | } 33 | }; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /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 7 | { 8 | public StackTrace() : base("stealth-stackTrace") { } 9 | 10 | public override async Task OnPageCreated(IPage page) 11 | { 12 | var script = Utils.GetScript("Stacktrace.js"); 13 | await page.EvaluateFunctionOnNewDocumentAsync(script); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/UserAgent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using System.Threading.Tasks; 5 | using PuppeteerSharp; 6 | 7 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions 8 | { 9 | public class UserAgent : PuppeteerExtraPlugin 10 | { 11 | private bool _isHeadless = false; 12 | 13 | public UserAgent() : base("stealth-userAgent") 14 | { 15 | } 16 | 17 | public override void BeforeLaunch(LaunchOptions options) 18 | { 19 | this._isHeadless = options.Headless; 20 | } 21 | 22 | public override async Task OnPageCreated(IPage page) 23 | { 24 | var ua = await page.Browser.GetUserAgentAsync(); 25 | ua = ua.Replace("HeadlessChrome/", "Chrome/"); 26 | var uaVersion = ua.Contains("Chrome/") 27 | ? Regex.Match(ua, @"Chrome\/([\d|.]+)").Groups[1].Value 28 | : Regex.Match(await page.Browser.GetVersionAsync(), @"\/([\d|.]+)").Groups[1].Value; 29 | 30 | var platform = GetPlatform(ua); 31 | var brand = GetBrands(uaVersion); 32 | 33 | var isMobile = GetIsMobile(ua); 34 | var platformVersion = GetPlatformVersion(ua); 35 | var platformArch = GetPlatformArch(isMobile); 36 | var platformModel = GetPlatformModel(isMobile, ua); 37 | 38 | var overrideObject = new OverrideUserAgent() 39 | { 40 | UserAgent = ua, 41 | Platform = platform, 42 | AcceptLanguage = "en-US, en", 43 | UserAgentMetadata = new UserAgentMetadata() 44 | { 45 | Brands = brand, 46 | FullVersion = uaVersion, 47 | Platform = platform, 48 | PlatformVersion = platformVersion, 49 | Architecture = platformArch, 50 | Model = platformModel, 51 | Mobile = isMobile 52 | } 53 | }; 54 | // 55 | // if (this._isHeadless) 56 | // { 57 | // var dynamicObject = overrideObject as dynamic; 58 | // dynamicObject.AcceptLanguage = "en-US, en"; 59 | // overrideObject = dynamicObject; 60 | // } 61 | 62 | await page.Client.SendAsync("Network.setUserAgentOverride", overrideObject); 63 | } 64 | 65 | private string GetPlatform(string ua) 66 | { 67 | if (ua.Contains("Mac OS X")) 68 | { 69 | return "Mac OS X"; 70 | } 71 | 72 | if (ua.Contains("Android")) 73 | { 74 | return "Android"; 75 | } 76 | 77 | if (ua.Contains("Linux")) 78 | { 79 | return "Linux"; 80 | } 81 | 82 | return "Windows"; 83 | } 84 | 85 | public string GetPlatformVersion(string ua) 86 | { 87 | if (ua.Contains("Mac OS X ")) 88 | { 89 | return Regex.Match(ua, "Mac OS X ([^)]+)").Groups[1].Value; 90 | } 91 | 92 | if (ua.Contains("Android ")) 93 | { 94 | return Regex.Match(ua, "Android ([^;]+)").Groups[1].Value; 95 | } 96 | 97 | if (ua.Contains("Windows ")) 98 | { 99 | return Regex.Match(ua, @"Windows .*?([\d|.]+);").Groups[1].Value; 100 | } 101 | 102 | return string.Empty; 103 | } 104 | 105 | public string GetPlatformArch(bool isMobile) 106 | { 107 | return isMobile ? string.Empty : "x86"; 108 | } 109 | 110 | public string GetPlatformModel(bool isMobile, string ua) 111 | { 112 | return isMobile ? Regex.Match(ua, @"Android.*?;\s([^)]+)").Groups[1].Value : string.Empty; 113 | } 114 | 115 | public bool GetIsMobile(string ua) 116 | { 117 | return ua.Contains("Android"); 118 | } 119 | 120 | private List GetBrands(string uaVersion) 121 | { 122 | var seed = int.Parse(uaVersion.Split('.')[0]); 123 | 124 | var order = new List> 125 | { 126 | new List() 127 | { 128 | 0, 1, 2 129 | }, 130 | new List() 131 | { 132 | 0, 2, 1 133 | }, 134 | new List() 135 | { 136 | 1, 0, 2 137 | }, 138 | new List() 139 | { 140 | 1, 2, 0 141 | }, 142 | new List() 143 | { 144 | 2, 0, 1 145 | }, 146 | new List() 147 | { 148 | 2, 1, 0 149 | }, 150 | }[seed % 6]; 151 | 152 | var escapedChars = new List() 153 | { 154 | " ", 155 | " ", 156 | ";" 157 | }; 158 | 159 | var greaseyBrand = $"{escapedChars[order[0]]}Not{escapedChars[order[1]]}A{escapedChars[order[2]]}Brand"; 160 | var greasedBrandVersionList = new Dictionary(); 161 | 162 | greasedBrandVersionList.Add(order[0], new UserAgentBrand() 163 | { 164 | Brand = greaseyBrand, 165 | Version = "99" 166 | }); 167 | greasedBrandVersionList.Add(order[1], new UserAgentBrand() 168 | { 169 | Brand = "Chromium", 170 | Version = seed.ToString() 171 | }); 172 | greasedBrandVersionList.Add(order[2], new UserAgentBrand() 173 | { 174 | Brand = "Google Chrome", 175 | Version = seed.ToString() 176 | }); 177 | 178 | return greasedBrandVersionList.OrderBy(e=>e.Key).Select(e=>e.Value).ToList(); 179 | } 180 | 181 | private class OverrideUserAgent 182 | { 183 | public string UserAgent { get; set; } 184 | public string Platform { get; set; } 185 | public string AcceptLanguage { get; set; } 186 | public UserAgentMetadata UserAgentMetadata { get; set; } 187 | } 188 | 189 | private class UserAgentMetadata 190 | { 191 | public List Brands { get; set; } 192 | public string FullVersion { get; set; } 193 | public string Platform { get; set; } 194 | public string PlatformVersion { get; set; } 195 | public string Architecture { get; set; } 196 | public string Model { get; set; } 197 | public bool Mobile { get; set; } 198 | } 199 | 200 | private class UserAgentBrand 201 | { 202 | public string Brand { get; set; } 203 | public string Version { get; set; } 204 | } 205 | } 206 | 207 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Evasions/Vendor.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Threading.Tasks; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions 6 | { 7 | public class Vendor : PuppeteerExtraPlugin 8 | { 9 | private readonly StealthVendorSettings _settings; 10 | public Vendor(StealthVendorSettings settings = null) : base("stealth-vendor") 11 | { 12 | _settings = settings ?? new StealthVendorSettings("Google Inc."); 13 | } 14 | 15 | public override async Task OnPageCreated(IPage page) 16 | { 17 | var script = Utils.GetScript("Vendor.js"); 18 | await page.EvaluateFunctionOnNewDocumentAsync(script, _settings.Vendor); 19 | } 20 | } 21 | 22 | public class StealthVendorSettings : IPuppeteerExtraPluginOptions 23 | { 24 | public string Vendor { get; } 25 | 26 | public StealthVendorSettings(string vendor) 27 | { 28 | Vendor = vendor; 29 | } 30 | } 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 8 | { 9 | public WebDriver() : base("stealth-webDriver") { } 10 | 11 | public override async Task OnPageCreated(IPage page) 12 | { 13 | var script = Utils.GetScript("WebDriver.js"); 14 | await page.EvaluateFunctionOnNewDocumentAsync(script); 15 | } 16 | 17 | public override void BeforeLaunch(LaunchOptions options) 18 | { 19 | var args = options.Args.ToList(); 20 | var idx = args.FindIndex(e => e.StartsWith("--disable-blink-features=")); 21 | if (idx != -1) 22 | { 23 | var arg = args[idx]; 24 | args[idx] = $"{arg}, AutomationControlled"; 25 | return; 26 | } 27 | 28 | args.Add("--disable-blink-features=AutomationControlled"); 29 | 30 | options.Args = args.ToArray(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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 : PuppeteerExtraPlugin 7 | { 8 | private readonly StealthWebGLOptions _options; 9 | public WebGl(StealthWebGLOptions options) : base("stealth-webGl") 10 | { 11 | _options = options ?? new StealthWebGLOptions("Intel Inc.", "Intel Iris OpenGL Engine"); 12 | } 13 | 14 | public override async Task OnPageCreated(IPage page) 15 | { 16 | var script = Utils.GetScript("WebGL.js"); 17 | await page.EvaluateFunctionOnNewDocumentAsync(script, _options.Vendor, _options.Renderer); 18 | } 19 | } 20 | 21 | public class StealthWebGLOptions : IPuppeteerExtraPluginOptions 22 | { 23 | public string Vendor { get; } 24 | public string Renderer { get; } 25 | 26 | public StealthWebGLOptions(string vendor, string renderer) 27 | { 28 | Vendor = vendor; 29 | Renderer = renderer; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/ChromeApp.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 ('app' in window.chrome) { 15 | return // Nothing to do here 16 | } 17 | 18 | const makeError = { 19 | ErrorInInvocation: fn => { 20 | const err = new TypeError(`Error in invocation of app.${fn}()`) 21 | return utils.stripErrorWithAnchor( 22 | err, 23 | `at ${fn} (eval at ` 24 | ) 25 | } 26 | } 27 | 28 | // There's a some static data in that property which doesn't seem to change, 29 | // we should periodically check for updates: `JSON.stringify(window.app, null, 2)` 30 | const STATIC_DATA = JSON.parse( 31 | ` 32 | { 33 | "isInstalled": false, 34 | "InstallState": { 35 | "DISABLED": "disabled", 36 | "INSTALLED": "installed", 37 | "NOT_INSTALLED": "not_installed" 38 | }, 39 | "RunningState": { 40 | "CANNOT_RUN": "cannot_run", 41 | "READY_TO_RUN": "ready_to_run", 42 | "RUNNING": "running" 43 | } 44 | } 45 | `.trim() 46 | ) 47 | 48 | window.chrome.app = { 49 | ...STATIC_DATA, 50 | 51 | get isInstalled() { 52 | return false 53 | }, 54 | 55 | getDetails: function getDetails() { 56 | if (arguments.length) { 57 | throw makeError.ErrorInInvocation(`getDetails`) 58 | } 59 | return null 60 | }, 61 | getIsInstalled: function getDetails() { 62 | if (arguments.length) { 63 | throw makeError.ErrorInInvocation(`getIsInstalled`) 64 | } 65 | return false 66 | }, 67 | runningState: function getDetails() { 68 | if (arguments.length) { 69 | throw makeError.ErrorInInvocation(`runningState`) 70 | } 71 | return 'cannot_run' 72 | } 73 | } 74 | utils.patchToStringNested(window.chrome.app) 75 | } -------------------------------------------------------------------------------- /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/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/ExtraStealth/Scripts/HardwareConcurrency.js: -------------------------------------------------------------------------------- 1 | (concurrency) => { 2 | 3 | utils.replaceGetterWithProxy( 4 | Object.getPrototypeOf(navigator), 5 | 'hardwareConcurrency', 6 | utils.makeHandler().getterValue(concurrency) 7 | ) 8 | } -------------------------------------------------------------------------------- /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/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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/Plugin.js: -------------------------------------------------------------------------------- 1 | () => { 2 | function generateFunctionMocks() { 3 | return ( 4 | proto, 5 | itemMainProp, 6 | dataArray 7 | ) => ({ 8 | /** Returns the MimeType object with the specified index. */ 9 | item: utils.createProxy(proto.item, { 10 | apply(target, ctx, args) { 11 | if (!args.length) { 12 | throw new TypeError( 13 | `Failed to execute 'item' on '${ 14 | proto[Symbol.toStringTag] 15 | }': 1 argument required, but only 0 present.` 16 | ) 17 | } 18 | // Special behavior alert: 19 | // - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup 20 | // - If anything else than an integer (including as string) is provided it will return the first entry 21 | const isInteger = args[0] && Number.isInteger(Number(args[0])) // Cast potential string to number first, then check for integer 22 | // Note: Vanilla never returns `undefined` 23 | return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null 24 | } 25 | }), 26 | /** Returns the MimeType object with the specified name. */ 27 | namedItem: utils.createProxy(proto.namedItem, { 28 | apply(target, ctx, args) { 29 | if (!args.length) { 30 | throw new TypeError( 31 | `Failed to execute 'namedItem' on '${ 32 | proto[Symbol.toStringTag] 33 | }': 1 argument required, but only 0 present.` 34 | ) 35 | } 36 | return dataArray.find(mt => mt[itemMainProp] === args[0]) || null // Not `undefined`! 37 | } 38 | }), 39 | /** Does nothing and shall return nothing */ 40 | refresh: proto.refresh 41 | ? utils.createProxy(proto.refresh, { 42 | apply(target, ctx, args) { 43 | return undefined 44 | } 45 | }) 46 | : undefined 47 | }) 48 | } 49 | 50 | function generateMagicArray() { 51 | return ( 52 | dataArray = [], 53 | proto = MimeTypeArray.prototype, 54 | itemProto = MimeType.prototype, 55 | itemMainProp = 'type' 56 | ) => { 57 | // Quick helper to set props with the same descriptors vanilla is using 58 | const defineProp = (obj, prop, value) => 59 | Object.defineProperty(obj, prop, { 60 | value, 61 | writable: false, 62 | enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)` 63 | configurable: true 64 | }) 65 | 66 | // Loop over our fake data and construct items 67 | const makeItem = data => { 68 | const item = {} 69 | for (const prop of Object.keys(data)) { 70 | if (prop.startsWith('__')) { 71 | continue 72 | } 73 | defineProp(item, prop, data[prop]) 74 | } 75 | return patchItem(item, data) 76 | } 77 | 78 | const patchItem = (item, data) => { 79 | let descriptor = Object.getOwnPropertyDescriptors(item) 80 | 81 | // Special case: Plugins have a magic length property which is not enumerable 82 | // e.g. `navigator.plugins[i].length` should always be the length of the assigned mimeTypes 83 | if (itemProto === Plugin.prototype) { 84 | descriptor = { 85 | ...descriptor, 86 | length: { 87 | value: data.__mimeTypes.length, 88 | writable: false, 89 | enumerable: false, 90 | configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length` 91 | } 92 | } 93 | } 94 | 95 | // We need to spoof a specific `MimeType` or `Plugin` object 96 | const obj = Object.create(itemProto, descriptor) 97 | 98 | // Virtually all property keys are not enumerable in vanilla 99 | const blacklist = [...Object.keys(data), 'length', 'enabledPlugin'] 100 | return new Proxy(obj, { 101 | ownKeys(target) { 102 | return Reflect.ownKeys(target).filter(k => !blacklist.includes(k)) 103 | }, 104 | getOwnPropertyDescriptor(target, prop) { 105 | if (blacklist.includes(prop)) { 106 | return undefined 107 | } 108 | return Reflect.getOwnPropertyDescriptor(target, prop) 109 | } 110 | }) 111 | } 112 | 113 | const magicArray = [] 114 | 115 | // Loop through our fake data and use that to create convincing entities 116 | dataArray.forEach(data => { 117 | magicArray.push(makeItem(data)) 118 | }) 119 | 120 | // Add direct property access based on types (e.g. `obj['application/pdf']`) afterwards 121 | magicArray.forEach(entry => { 122 | defineProp(magicArray, entry[itemMainProp], entry) 123 | }) 124 | 125 | // This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)` 126 | const magicArrayObj = Object.create(proto, { 127 | ...Object.getOwnPropertyDescriptors(magicArray), 128 | 129 | // There's one ugly quirk we unfortunately need to take care of: 130 | // The `MimeTypeArray` prototype has an enumerable `length` property, 131 | // but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`. 132 | // To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap. 133 | length: { 134 | value: magicArray.length, 135 | writable: false, 136 | enumerable: false, 137 | configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length` 138 | } 139 | }) 140 | 141 | // Generate our functional function mocks :-) 142 | const functionMocks = generateFunctionMocks()( 143 | proto, 144 | itemMainProp, 145 | magicArray 146 | ) 147 | 148 | // We need to overlay our custom object with a JS Proxy 149 | const magicArrayObjProxy = new Proxy(magicArrayObj, { 150 | get(target, key = '') { 151 | // Redirect function calls to our custom proxied versions mocking the vanilla behavior 152 | if (key === 'item') { 153 | return functionMocks.item 154 | } 155 | if (key === 'namedItem') { 156 | return functionMocks.namedItem 157 | } 158 | if (proto === PluginArray.prototype && key === 'refresh') { 159 | return functionMocks.refresh 160 | } 161 | // Everything else can pass through as normal 162 | return utils.cache.Reflect.get(...arguments) 163 | }, 164 | ownKeys(target) { 165 | // There are a couple of quirks where the original property demonstrates "magical" behavior that makes no sense 166 | // This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length` 167 | // My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly 168 | // For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing 169 | // Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing 170 | const keys = [] 171 | const typeProps = magicArray.map(mt => mt[itemMainProp]) 172 | typeProps.forEach((_, i) => keys.push(`${i}`)) 173 | typeProps.forEach(propName => keys.push(propName)) 174 | return keys 175 | }, 176 | getOwnPropertyDescriptor(target, prop) { 177 | if (prop === 'length') { 178 | return undefined 179 | } 180 | return Reflect.getOwnPropertyDescriptor(target, prop) 181 | } 182 | }) 183 | 184 | return magicArrayObjProxy 185 | } 186 | } 187 | 188 | function generateMimeTypeArray() { 189 | return mimeTypesData => { 190 | return generateMagicArray()( 191 | mimeTypesData, 192 | MimeTypeArray.prototype, 193 | MimeType.prototype, 194 | 'type' 195 | ) 196 | } 197 | } 198 | 199 | function generatePluginArray() { 200 | return pluginsData => { 201 | return generateMagicArray()( 202 | pluginsData, 203 | PluginArray.prototype, 204 | Plugin.prototype, 205 | 'name' 206 | ) 207 | } 208 | } 209 | 210 | 211 | { 212 | // That means we're running headful 213 | const hasPlugins = 'plugins' in navigator && navigator.plugins.length 214 | if (hasPlugins) { 215 | return // nothing to do here 216 | } 217 | 218 | const dataMimeTypes = 219 | [ 220 | { 221 | "type": "application/pdf", 222 | "suffixes": "pdf", 223 | "description": "", 224 | "__pluginName": "Chrome PDF Viewer" 225 | }, 226 | { 227 | "type": "application/x-google-chrome-pdf", 228 | "suffixes": "pdf", 229 | "description": "Portable Document Format", 230 | "__pluginName": "Chrome PDF Plugin" 231 | }, 232 | { 233 | "type": "application/x-nacl", 234 | "suffixes": "", 235 | "description": "Native Client Executable", 236 | "__pluginName": "Native Client" 237 | }, 238 | { 239 | "type": "application/x-pnacl", 240 | "suffixes": "", 241 | "description": "Portable Native Client Executable", 242 | "__pluginName": "Native Client" 243 | } 244 | ]; 245 | const dataPlugins = 246 | [ 247 | { 248 | "name": "Chrome PDF Plugin", 249 | "filename": "internal-pdf-viewer", 250 | "description": "Portable Document Format", 251 | "__mimeTypes": ["application/x-google-chrome-pdf"] 252 | }, 253 | { 254 | "name": "Chrome PDF Viewer", 255 | "filename": "mhjfbmdgcfjbbpaeojofohoefgiehjai", 256 | "description": "", 257 | "__mimeTypes": ["application/pdf"] 258 | }, 259 | { 260 | "name": "Native Client", 261 | "filename": "internal-nacl-plugin", 262 | "description": "", 263 | "__mimeTypes": ["application/x-nacl", "application/x-pnacl"] 264 | } 265 | ] 266 | 267 | const mimeTypes = generateMimeTypeArray()(dataMimeTypes) 268 | const plugins = generatePluginArray()(dataPlugins) 269 | 270 | // Plugin and MimeType cross-reference each other, let's do that now 271 | // Note: We're looping through `data.plugins` here, not the generated `plugins` 272 | for (const pluginData of dataPlugins) { 273 | pluginData.__mimeTypes.forEach((type, index) => { 274 | plugins[pluginData.name][index] = mimeTypes[type] 275 | 276 | Object.defineProperty(plugins[pluginData.name], type, { 277 | value: mimeTypes[type], 278 | writable: false, 279 | enumerable: false, // Not enumerable 280 | configurable: true 281 | }) 282 | Object.defineProperty(mimeTypes[type], 'enabledPlugin', { 283 | value: 284 | type === 'application/x-pnacl' 285 | ? mimeTypes['application/x-nacl'].enabledPlugin // these reference the same plugin, so we need to re-use the Proxy in order to avoid leaks 286 | : new Proxy(plugins[pluginData.name], {}), // Prevent circular references 287 | writable: false, 288 | enumerable: false, // Important: `JSON.stringify(navigator.plugins)` 289 | configurable: true 290 | }) 291 | }) 292 | } 293 | 294 | const patchNavigator = (name, value) => 295 | utils.replaceProperty(Object.getPrototypeOf(navigator), name, { 296 | get() { 297 | return value 298 | } 299 | }) 300 | 301 | patchNavigator('mimeTypes', mimeTypes) 302 | patchNavigator('plugins', plugins) 303 | 304 | // All done 305 | } 306 | 307 | 308 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Scripts/Runtime.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 | const existsAlready = 'runtime' in window.chrome 15 | // `chrome.runtime` is only exposed on secure origins 16 | const isNotSecure = !window.location.protocol.startsWith('https') 17 | if (existsAlready || isNotSecure) { 18 | return // Nothing to do here 19 | } 20 | window.chrome.runtime = { 21 | // There's a bunch of static data in that property which doesn't seem to change, 22 | // we should periodically check for updates: `JSON.stringify(window.chrome.runtime, null, 2)` 23 | "OnInstalledReason": { 24 | "CHROME_UPDATE": "chrome_update", 25 | "INSTALL": "install", 26 | "SHARED_MODULE_UPDATE": "shared_module_update", 27 | "UPDATE": "update" 28 | }, 29 | "OnRestartRequiredReason": { 30 | "APP_UPDATE": "app_update", 31 | "OS_UPDATE": "os_update", 32 | "PERIODIC": "periodic" 33 | }, 34 | "PlatformArch": { 35 | "ARM": "arm", 36 | "ARM64": "arm64", 37 | "MIPS": "mips", 38 | "MIPS64": "mips64", 39 | "X86_32": "x86-32", 40 | "X86_64": "x86-64" 41 | }, 42 | "PlatformNaclArch": { 43 | "ARM": "arm", 44 | "MIPS": "mips", 45 | "MIPS64": "mips64", 46 | "X86_32": "x86-32", 47 | "X86_64": "x86-64" 48 | }, 49 | "PlatformOs": { 50 | "ANDROID": "android", 51 | "CROS": "cros", 52 | "LINUX": "linux", 53 | "MAC": "mac", 54 | "OPENBSD": "openbsd", 55 | "WIN": "win" 56 | }, 57 | "RequestUpdateCheckStatus": { 58 | "NO_UPDATE": "no_update", 59 | "THROTTLED": "throttled", 60 | "UPDATE_AVAILABLE": "update_available" 61 | }, 62 | // `chrome.runtime.id` is extension related and returns undefined in Chrome 63 | get id() { 64 | return undefined 65 | }, 66 | // These two require more sophisticated mocks 67 | connect: null, 68 | sendMessage: null 69 | } 70 | 71 | const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({ 72 | NoMatchingSignature: new TypeError( 73 | preamble + `No matching signature.` 74 | ), 75 | MustSpecifyExtensionID: new TypeError( 76 | preamble + 77 | `${method} called from a webpage must specify an Extension ID (string) for its first argument.` 78 | ), 79 | InvalidExtensionID: new TypeError( 80 | preamble + `Invalid extension id: '${extensionId}'` 81 | ) 82 | }) 83 | 84 | // Valid Extension IDs are 32 characters in length and use the letter `a` to `p`: 85 | // https://source.chromium.org/chromium/chromium/src/+/master:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90 86 | const isValidExtensionID = str => 87 | str.length === 32 && str.toLowerCase().match(/^[a-p]+$/) 88 | 89 | /** Mock `chrome.runtime.sendMessage` */ 90 | const sendMessageHandler = { 91 | apply: function (target, ctx, args) { 92 | const [extensionId, options, responseCallback] = args || [] 93 | 94 | // Define custom errors 95 | const errorPreamble = `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): ` 96 | const Errors = makeCustomRuntimeErrors( 97 | errorPreamble, 98 | `chrome.runtime.sendMessage()`, 99 | extensionId 100 | ) 101 | 102 | // Check if the call signature looks ok 103 | const noArguments = args.length === 0 104 | const tooManyArguments = args.length > 4 105 | const incorrectOptions = options && typeof options !== 'object' 106 | const incorrectResponseCallback = 107 | responseCallback && typeof responseCallback !== 'function' 108 | if ( 109 | noArguments || 110 | tooManyArguments || 111 | incorrectOptions || 112 | incorrectResponseCallback 113 | ) { 114 | throw Errors.NoMatchingSignature 115 | } 116 | 117 | // At least 2 arguments are required before we even validate the extension ID 118 | if (args.length < 2) { 119 | throw Errors.MustSpecifyExtensionID 120 | } 121 | 122 | // Now let's make sure we got a string as extension ID 123 | if (typeof extensionId !== 'string') { 124 | throw Errors.NoMatchingSignature 125 | } 126 | 127 | if (!isValidExtensionID(extensionId)) { 128 | throw Errors.InvalidExtensionID 129 | } 130 | 131 | return undefined // Normal behavior 132 | } 133 | } 134 | utils.mockWithProxy( 135 | window.chrome.runtime, 136 | 'sendMessage', 137 | function sendMessage() { 138 | }, 139 | sendMessageHandler 140 | ) 141 | 142 | /** 143 | * Mock `chrome.runtime.connect` 144 | * 145 | * @see https://developer.chrome.com/apps/runtime#method-connect 146 | */ 147 | const connectHandler = { 148 | apply: function (target, ctx, args) { 149 | const [extensionId, connectInfo] = args || [] 150 | 151 | // Define custom errors 152 | const errorPreamble = `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): ` 153 | const Errors = makeCustomRuntimeErrors( 154 | errorPreamble, 155 | `chrome.runtime.connect()`, 156 | extensionId 157 | ) 158 | 159 | // Behavior differs a bit from sendMessage: 160 | const noArguments = args.length === 0 161 | const emptyStringArgument = args.length === 1 && extensionId === '' 162 | if (noArguments || emptyStringArgument) { 163 | throw Errors.MustSpecifyExtensionID 164 | } 165 | 166 | const tooManyArguments = args.length > 2 167 | const incorrectConnectInfoType = 168 | connectInfo && typeof connectInfo !== 'object' 169 | 170 | if (tooManyArguments || incorrectConnectInfoType) { 171 | throw Errors.NoMatchingSignature 172 | } 173 | 174 | const extensionIdIsString = typeof extensionId === 'string' 175 | if (extensionIdIsString && extensionId === '') { 176 | throw Errors.MustSpecifyExtensionID 177 | } 178 | if (extensionIdIsString && !isValidExtensionID(extensionId)) { 179 | throw Errors.InvalidExtensionID 180 | } 181 | 182 | // There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate 183 | const validateConnectInfo = ci => { 184 | // More than a first param connectInfo as been provided 185 | if (args.length > 1) { 186 | throw Errors.NoMatchingSignature 187 | } 188 | // An empty connectInfo has been provided 189 | if (Object.keys(ci).length === 0) { 190 | throw Errors.MustSpecifyExtensionID 191 | } 192 | // Loop over all connectInfo props an check them 193 | Object.entries(ci).forEach(([k, v]) => { 194 | const isExpected = ['name', 'includeTlsChannelId'].includes(k) 195 | if (!isExpected) { 196 | throw new TypeError( 197 | errorPreamble + `Unexpected property: '${k}'.` 198 | ) 199 | } 200 | const MismatchError = (propName, expected, found) => 201 | TypeError( 202 | errorPreamble + 203 | `Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.` 204 | ) 205 | if (k === 'name' && typeof v !== 'string') { 206 | throw MismatchError(k, 'string', typeof v) 207 | } 208 | if (k === 'includeTlsChannelId' && typeof v !== 'boolean') { 209 | throw MismatchError(k, 'boolean', typeof v) 210 | } 211 | }) 212 | } 213 | if (typeof extensionId === 'object') { 214 | validateConnectInfo(extensionId) 215 | throw Errors.MustSpecifyExtensionID 216 | } 217 | 218 | // Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well 219 | return utils.patchToStringNested(makeConnectResponse()) 220 | } 221 | } 222 | utils.mockWithProxy( 223 | window.chrome.runtime, 224 | 'connect', 225 | function connect() { 226 | }, 227 | connectHandler 228 | ) 229 | 230 | function makeConnectResponse() { 231 | const onSomething = () => ({ 232 | addListener: function addListener() { 233 | }, 234 | dispatch: function dispatch() { 235 | }, 236 | hasListener: function hasListener() { 237 | }, 238 | hasListeners: function hasListeners() { 239 | return false 240 | }, 241 | removeListener: function removeListener() { 242 | } 243 | }) 244 | 245 | const response = { 246 | name: '', 247 | sender: undefined, 248 | disconnect: function disconnect() { 249 | }, 250 | onDisconnect: onSomething(), 251 | onMessage: onSomething(), 252 | postMessage: function postMessage() { 253 | if (!arguments.length) { 254 | throw new TypeError(`Insufficient number of arguments.`) 255 | } 256 | throw new Error(`Attempting to use a disconnected port object`) 257 | } 258 | } 259 | return response 260 | } 261 | } -------------------------------------------------------------------------------- /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/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/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/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/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 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/StealthPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | using PuppeteerSharp; 6 | 7 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth 8 | { 9 | public class StealthPlugin : PuppeteerExtraPlugin 10 | { 11 | private readonly IPuppeteerExtraPluginOptions[] _options; 12 | private readonly List _standardEvasions; 13 | 14 | public StealthPlugin(params IPuppeteerExtraPluginOptions[] options) : base("stealth") 15 | { 16 | _options = options; 17 | _standardEvasions = GetStandardEvasions(); 18 | } 19 | 20 | private List GetStandardEvasions() 21 | { 22 | return new List() 23 | { 24 | new WebDriver(), 25 | // new ChromeApp(), 26 | new ChromeSci(), 27 | new ChromeRuntime(), 28 | new Codec(), 29 | new Languages(GetOptionByType()), 30 | new OutDimensions(), 31 | new Permissions(), 32 | new UserAgent(), 33 | new Vendor(GetOptionByType()), 34 | new WebGl(GetOptionByType()), 35 | new PluginEvasion(), 36 | new StackTrace(), 37 | new HardwareConcurrency(GetOptionByType()), 38 | new ContentWindow(), 39 | new SourceUrl() 40 | }; 41 | } 42 | 43 | public override ICollection GetDependencies() => _standardEvasions; 44 | 45 | public override async Task OnPageCreated(IPage page) 46 | { 47 | var utilsScript = Utils.GetScript("Utils.js"); 48 | await page.EvaluateExpressionOnNewDocumentAsync(utilsScript); 49 | } 50 | 51 | private T GetOptionByType() where T : IPuppeteerExtraPluginOptions 52 | { 53 | return _options.OfType().FirstOrDefault(); 54 | } 55 | 56 | public void RemoveEvasionByType() where T : PuppeteerExtraPlugin 57 | { 58 | _standardEvasions.RemoveAll(ev => ev is T); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using PuppeteerExtraSharp.Utils; 6 | using PuppeteerSharp; 7 | 8 | namespace PuppeteerExtraSharp.Plugins.ExtraStealth 9 | { 10 | internal static class Utils 11 | { 12 | public static Task EvaluateOnNewPage(IPage page, string script, params object[] args) 13 | { 14 | if (!page.IsClosed) 15 | return page.EvaluateFunctionOnNewDocumentAsync(script, args); 16 | 17 | return Task.CompletedTask; 18 | } 19 | 20 | 21 | public static string GetScript(string name) 22 | { 23 | var builder = new StringBuilder(typeof(Utils).Namespace); 24 | builder.Append(".Scripts"); 25 | builder.Append("." + name); 26 | 27 | var file = ResourcesReader.ReadFile(builder.ToString()); 28 | return file; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/ExtraStealth/readme.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | ```c# 3 | var extra = new PuppeteerExtra(); 4 | // initialize stealth plugin 5 | var stealth = new StealthPlugin(); 6 | 7 | var browser = await extra.Use(stealth).LaunchAsync(new LaunchOptions()); 8 | 9 | var page = await browser.NewPageAsync(); 10 | 11 | await page.GoToAsync("https://bot.sannysoft.com/"); 12 | ``` 13 | 14 | # Plugin options 15 | 16 | ```c# 17 | var extra = new PuppeteerExtra(); 18 | // Initialize hardware concurrency plugin options 19 | var stealthHardwareConcurrencyOptions = new StealthHardwareConcurrencyOptions(12); 20 | // Put it on stealth plugin constructor 21 | var stealth = new StealthPlugin(stealthHardwareConcurrencyOptions); 22 | var browser = await extra.Use(stealth).LaunchAsync(new LaunchOptions()); 23 | var page = await browser.NewPageAsync(); 24 | await page.GoToAsync("https://bot.sannysoft.com/"); 25 | ``` 26 | 27 | ### Available options: 28 | #### Hardware concurrency 29 | see (https://arh.antoinevastel.com/reports/stats/osName_hardwareConcurrency_report.html) 30 | ```c# 31 | var concurrency = 12; // your number 32 | var stealthHardwareConcurrencyOptions = new StealthHardwareConcurrencyOptions(concurrency); 33 | ``` 34 | #### Vendor 35 | ```c# 36 | var vendor = "Google Inc."; // your custom navigator.vendor 37 | var stealthVendorSettings = new StealthVendorSettings(vendor); 38 | ``` 39 | ### Languages 40 | ```c# 41 | var languages = "en-US"; // your custom languages array 42 | var languagesSettings = new StealthLanguagesOptions(languages); 43 | ``` 44 | 45 | ### WebGL 46 | ```c# 47 | var webGLVendor = "Intel Inc."; // your custom webGL vendor 48 | var render = "Intel Iris OpenGL Engine"; // your custom webGL renderer 49 | var languagesSettings = new StealthWebGLOptions(webGLVendor, render); 50 | ``` 51 | 52 | # Removing evasions: 53 | You can remove an evasion from the plugin by using the RemoveEvasionByType 54 | ```c# 55 | var extra = new PuppeteerExtra(); 56 | // initialize stealth plugin 57 | var stealth = new StealthPlugin(); 58 | stealthPlugin.RemoveEvasionByType(); 59 | var browser = await extra.Use(stealth).LaunchAsync(new LaunchOptions()); 60 | ``` -------------------------------------------------------------------------------- /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 8 | { 9 | protected PuppeteerExtraPlugin(string pluginName) 10 | { 11 | Name = pluginName; 12 | } 13 | 14 | public string Name { get; } 15 | 16 | public virtual List Requirements { get; set; } 17 | 18 | public virtual ICollection GetDependencies() 19 | { 20 | return null; 21 | } 22 | 23 | public virtual void BeforeLaunch(LaunchOptions options) { } 24 | public virtual void AfterLaunch(IBrowser browser) { } 25 | public virtual void BeforeConnect(ConnectOptions options) { } 26 | public virtual void AfterConnect(IBrowser browser) { } 27 | public virtual void OnBrowser(IBrowser browser) { } 28 | public virtual void OnTargetCreated(Target target) { } 29 | public virtual Task OnPageCreated(IPage page) { return Task.CompletedTask; } 30 | public virtual void OnTargetChanged(Target target) { } 31 | public virtual void OnTargetDestroyed(Target target) { } 32 | public virtual void OnDisconnected() { } 33 | public virtual void OnClose() { } 34 | public virtual void OnPluginRegistered() { } 35 | } 36 | 37 | 38 | public enum PluginRequirements 39 | { 40 | Launch, 41 | HeadFul, 42 | DataFromPlugin, 43 | RunLast 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/PuppeteerExtraPluginOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace PuppeteerExtraSharp.Plugins 6 | { 7 | public interface IPuppeteerExtraPluginOptions 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/CaptchaCfg.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha 2 | { 3 | public class CaptchaCfg 4 | { 5 | public string callback { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/CaptchaException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PuppeteerExtraSharp.Plugins.Recaptcha 4 | { 5 | public class CaptchaException: Exception 6 | { 7 | public CaptchaException(string pageUrl, string content) 8 | { 9 | PageUrl = pageUrl; 10 | Content = content; 11 | } 12 | 13 | public string PageUrl { get; set; } 14 | public string Content { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/CaptchaOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha 2 | { 3 | public class CaptchaOptions 4 | { 5 | public bool VisualFeedBack { get; set; } = false; 6 | public bool IsThrowException { get; set; } = false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/2Captcha/Models/TwoCaptchaRequest.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider._2Captcha.Models 2 | { 3 | internal class TwoCaptchaRequest 4 | { 5 | public string key { get; set; } 6 | } 7 | 8 | internal class TwoCaptchaTask: TwoCaptchaRequest 9 | { 10 | public string method { get; set; } = "userrecaptcha"; 11 | public string googlekey { get; set; } 12 | public string pageurl { get; set; } 13 | } 14 | 15 | internal class TwoCaptchaRequestForResult: TwoCaptchaRequest 16 | { 17 | public string action { get; set; } = "get"; 18 | public string id { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/2Captcha/Models/TwoCaptchaResponse.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider._2Captcha.Models 2 | { 3 | internal class TwoCaptchaResponse 4 | { 5 | public int status { get; set; } 6 | public string request { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/2Captcha/TwoCaptcha.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider._2Captcha.Models; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider._2Captcha 6 | { 7 | public class TwoCaptcha : IRecaptchaProvider 8 | { 9 | private readonly ProviderOptions _options; 10 | private readonly TwoCaptchaApi _api; 11 | 12 | public TwoCaptcha(string key, ProviderOptions options = null) 13 | { 14 | _options = options ?? ProviderOptions.CreateDefaultOptions(); 15 | _api = new TwoCaptchaApi(key, _options); 16 | } 17 | 18 | public async Task GetSolution(string key, string pageUrl, string proxyStr = null) 19 | { 20 | var task = await _api.CreateTaskAsync(key, pageUrl); 21 | 22 | ThrowErrorIfBadStatus(task); 23 | 24 | await Task.Delay(_options.StartTimeoutSeconds * 1000); 25 | 26 | var result = await _api.GetSolution(task.request); 27 | 28 | ThrowErrorIfBadStatus(result.Data); 29 | 30 | return result.Data.request; 31 | } 32 | 33 | private void ThrowErrorIfBadStatus(TwoCaptchaResponse response) 34 | { 35 | if (response.status != 1 || string.IsNullOrEmpty(response.request)) 36 | throw new HttpRequestException($"Two captcha request ends with error [{response.status}] {response.request}"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/2Captcha/TwoCaptchaApi.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider._2Captcha.Models; 4 | using PuppeteerExtraSharp.Plugins.Recaptcha.RestClient; 5 | using PuppeteerExtraSharp.Utils; 6 | using RestSharp; 7 | 8 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider._2Captcha 9 | { 10 | internal class TwoCaptchaApi 11 | { 12 | private readonly RestClient.RestClient _client = new RestClient.RestClient("https://rucaptcha.com"); 13 | private readonly string _userKey; 14 | private readonly ProviderOptions _options; 15 | 16 | public TwoCaptchaApi(string userKey, ProviderOptions options) 17 | { 18 | _userKey = userKey; 19 | _options = options; 20 | } 21 | 22 | public async Task CreateTaskAsync(string key, string pageUrl) 23 | { 24 | var result = await _client.PostWithQueryAsync("in.php", new Dictionary() 25 | { 26 | ["key"] = _userKey, 27 | ["googlekey"] = key, 28 | ["pageurl"] = pageUrl, 29 | ["json"] = "1", 30 | ["method"] = "userrecaptcha" 31 | }); 32 | 33 | return result; 34 | } 35 | 36 | 37 | public async Task> GetSolution(string id) 38 | { 39 | var request = new RestRequest("res.php") {Method = Method.Post}; 40 | 41 | request.AddQueryParameter("id", id); 42 | request.AddQueryParameter("key", _userKey); 43 | request.AddQueryParameter("action", "get"); 44 | request.AddQueryParameter("json", "1"); 45 | 46 | var result = await _client.CreatePollingBuilder(request).TriesLimit(_options.PendingCount).ActivatePollingAsync( 47 | response => response.Data.request == "CAPCHA_NOT_READY" ? PollingAction.ContinuePolling : PollingAction.Break); 48 | 49 | return result; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/AntiCaptcha/AntiCaptcha.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | 4 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha 5 | { 6 | public class AntiCaptcha : IRecaptchaProvider 7 | { 8 | private readonly ProviderOptions _options; 9 | private readonly AntiCaptchaApi _api; 10 | public AntiCaptcha(string userKey, ProviderOptions options = null) 11 | { 12 | _options = options ?? ProviderOptions.CreateDefaultOptions(); 13 | _api = new AntiCaptchaApi(userKey, _options); 14 | } 15 | public async Task GetSolution(string key, string pageUrl, string proxyStr = null) 16 | { 17 | var task = await _api.CreateTaskAsync(pageUrl, key); 18 | await System.Threading.Tasks.Task.Delay(_options.StartTimeoutSeconds * 1000); 19 | var result = await _api.PendingForResult(task.taskId); 20 | 21 | if (result.status != "ready" || result.solution is null || result.errorId != 0) 22 | throw new HttpRequestException($"AntiCaptcha request ends with error - {result.errorId}"); 23 | 24 | return result.solution.gRecaptchaResponse; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/AntiCaptcha/AntiCaptchaApi.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha.Models; 4 | using PuppeteerExtraSharp.Plugins.Recaptcha.RestClient; 5 | using RestSharp; 6 | 7 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha 8 | { 9 | public class AntiCaptchaApi 10 | { 11 | private readonly string _userKey; 12 | private readonly ProviderOptions _options; 13 | private readonly RestClient.RestClient _client = new RestClient.RestClient("http://api.anti-captcha.com"); 14 | public AntiCaptchaApi(string userKey, ProviderOptions options) 15 | { 16 | _userKey = userKey; 17 | _options = options; 18 | } 19 | 20 | public Task CreateTaskAsync(string pageUrl, string key, CancellationToken token = default) 21 | { 22 | var content = new AntiCaptchaRequest() 23 | { 24 | clientKey = _userKey, 25 | task = new AntiCaptchaTask() 26 | { 27 | type = "NoCaptchaTaskProxyless", 28 | websiteURL = pageUrl, 29 | websiteKey = key 30 | } 31 | }; 32 | 33 | 34 | 35 | var result = _client.PostWithJsonAsync("createTask", content, token); 36 | return result; 37 | } 38 | 39 | 40 | public async Task PendingForResult(int taskId, CancellationToken token = default) 41 | { 42 | var content = new RequestForResultTask() 43 | { 44 | clientKey = _userKey, 45 | taskId = taskId 46 | }; 47 | 48 | 49 | var request = new RestRequest("getTaskResult"); 50 | request.AddJsonBody(content); 51 | request.Method = Method.Post; 52 | 53 | var result = await _client.CreatePollingBuilder(request).TriesLimit(_options.PendingCount) 54 | .WithTimeoutSeconds(5).ActivatePollingAsync( 55 | response => 56 | { 57 | if (response.Data.status == "ready" || response.Data.errorId != 0) 58 | return PollingAction.Break; 59 | 60 | return PollingAction.ContinuePolling; 61 | }); 62 | return result.Data; 63 | } 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/AntiCaptcha/Models/AntiCaptchaRequest.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | // ReSharper disable InconsistentNaming 4 | // ReSharper disable IdentifierTypo 5 | 6 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha.Models 7 | { 8 | public class AntiCaptchaRequest 9 | { 10 | public string clientKey { get; set; } 11 | public AntiCaptchaTask task { get; set; } 12 | } 13 | 14 | public class RequestForResultTask 15 | { 16 | public string clientKey { get; set; } 17 | public int taskId { get; set; } 18 | } 19 | 20 | public class AntiCaptchaTaskResult 21 | { 22 | public int errorId { get; set; } 23 | public int taskId { get; set; } 24 | } 25 | 26 | 27 | public class AntiCaptchaTask 28 | { 29 | public string type { get; set; } 30 | public string websiteURL { get; set; } 31 | public string websiteKey { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/AntiCaptcha/Models/TaskResultModel.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha.Models 2 | { 3 | public class TaskResultModel 4 | { 5 | public int errorId { get; set; } 6 | public string status { get; set; } 7 | public Solution solution { get; set; } 8 | public string cost { get; set; } 9 | public string ip { get; set; } 10 | public int createTime { get; set; } 11 | public int endTime { get; set; } 12 | public int solveCount { get; set; } 13 | } 14 | 15 | public class Solution 16 | { 17 | public string gRecaptchaResponse { get; set; } 18 | public Cookies cookies { get; set; } 19 | } 20 | 21 | public class Cookies 22 | { 23 | public string empty { get; set; } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /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 GetSolution(string key, string pageUrl, string proxyStr = null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Provider/ProviderOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.Provider 2 | { 3 | public class ProviderOptions 4 | { 5 | public int PendingCount { get; set; } 6 | public int StartTimeoutSeconds { get; set; } 7 | 8 | public static ProviderOptions CreateDefaultOptions() 9 | { 10 | return new ProviderOptions() 11 | { 12 | PendingCount = 30, 13 | StartTimeoutSeconds = 30 14 | }; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/RecapchaPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 3 | using PuppeteerSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.Recaptcha 6 | { 7 | public class RecaptchaPlugin : PuppeteerExtraPlugin 8 | { 9 | private readonly Recaptcha _recaptcha; 10 | 11 | public RecaptchaPlugin(IRecaptchaProvider provider, CaptchaOptions opt = null) : base("recaptcha") 12 | { 13 | _recaptcha = new Recaptcha(provider, opt ?? new CaptchaOptions()); 14 | } 15 | 16 | public async Task SolveCaptchaAsync(IPage page) 17 | { 18 | return await _recaptcha.Solve(page); 19 | } 20 | 21 | public override async Task OnPageCreated(IPage page) 22 | { 23 | await page.SetBypassCSPAsync(true); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Recaptcha.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Web; 4 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 5 | using PuppeteerExtraSharp.Utils; 6 | using PuppeteerSharp; 7 | 8 | namespace PuppeteerExtraSharp.Plugins.Recaptcha 9 | { 10 | public class Recaptcha 11 | { 12 | private readonly IRecaptchaProvider _provider; 13 | private readonly CaptchaOptions _options; 14 | 15 | public Recaptcha(IRecaptchaProvider provider, CaptchaOptions options) 16 | { 17 | _provider = provider; 18 | _options = options; 19 | } 20 | 21 | public async Task Solve(IPage page) 22 | { 23 | try 24 | { 25 | var key = await GetKeyAsync(page); 26 | var solution = await GetSolutionAsync(key, page.Url); 27 | await WriteToInput(page, solution); 28 | 29 | return new RecaptchaResult() 30 | { 31 | IsSuccess = true 32 | }; 33 | } 34 | catch (CaptchaException ex) 35 | { 36 | return new RecaptchaResult() 37 | { 38 | Exception = ex, 39 | IsSuccess = false 40 | }; 41 | } 42 | 43 | } 44 | 45 | public async Task GetKeyAsync(IPage page) 46 | { 47 | var element = 48 | await page.QuerySelectorAsync("iframe[src^='https://www.google.com/recaptcha/api2/anchor'][name^=\"a-\"]"); 49 | 50 | if (element == null) 51 | throw new CaptchaException(page.Url, "Recaptcha key not found!"); 52 | 53 | var src = await element.GetPropertyAsync("src"); 54 | 55 | if (src == null) 56 | throw new CaptchaException(page.Url, "Recaptcha key not found!"); 57 | 58 | var key = HttpUtility.ParseQueryString(src.ToString()).Get("k"); 59 | return key; 60 | } 61 | 62 | public async Task GetSolutionAsync(string key, string urlPage) 63 | { 64 | return await _provider.GetSolution(key, urlPage); 65 | } 66 | 67 | public async Task WriteToInput(IPage page, string value) 68 | { 69 | await page.EvaluateFunctionAsync( 70 | $"() => {{document.getElementById('g-recaptcha-response').innerHTML='{value}'}}"); 71 | 72 | 73 | var script = ResourcesReader.ReadFile(this.GetType().Namespace + ".Scripts.EnterRecaptchaCallBackScript.js"); 74 | 75 | try 76 | { 77 | await page.EvaluateFunctionAsync($@"(value) => {{{script}}}", value); 78 | } 79 | catch 80 | { 81 | // ignored 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/RecaptchaResult.cs: -------------------------------------------------------------------------------- 1 | namespace PuppeteerExtraSharp.Plugins.Recaptcha 2 | { 3 | public class RecaptchaResult 4 | { 5 | public bool IsSuccess { get; set; } = true; 6 | public CaptchaException Exception { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/RestClient/PollingBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using RestSharp; 4 | 5 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.RestClient 6 | { 7 | public class PollingBuilder 8 | { 9 | private readonly RestSharp.RestClient _client; 10 | private readonly RestSharp.RestRequest _request; 11 | private int _timeout = 5; 12 | private int _limit = 5; 13 | public PollingBuilder(RestSharp.RestClient client, RestRequest request) 14 | { 15 | _client = client; 16 | _request = request; 17 | } 18 | 19 | public PollingBuilder WithTimeoutSeconds(int timeout) 20 | { 21 | _timeout = timeout; 22 | return this; 23 | } 24 | 25 | public PollingBuilder TriesLimit(int limit) 26 | { 27 | _limit = limit; 28 | return this; 29 | } 30 | 31 | public async Task> ActivatePollingAsync(Func, PollingAction> resultDelegate) 32 | { 33 | var response = await _client.ExecuteAsync(_request); 34 | 35 | if (resultDelegate(response) == PollingAction.Break || _limit <= 1) 36 | return response; 37 | 38 | await Task.Delay(_timeout * 1000); 39 | _limit -= 1; 40 | 41 | return await ActivatePollingAsync(resultDelegate); 42 | } 43 | } 44 | 45 | public enum PollingAction 46 | { 47 | ContinuePolling, 48 | Break 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/RestClient/RestClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Utils; 5 | using RestSharp; 6 | 7 | namespace PuppeteerExtraSharp.Plugins.Recaptcha.RestClient 8 | { 9 | public class RestClient 10 | { 11 | private readonly RestSharp.RestClient _client; 12 | 13 | public RestClient(string url = null) 14 | { 15 | _client = string.IsNullOrWhiteSpace(url) ? new RestSharp.RestClient() : new RestSharp.RestClient(url); 16 | } 17 | 18 | public PollingBuilder CreatePollingBuilder(RestRequest request) 19 | { 20 | return new PollingBuilder(_client, request); 21 | } 22 | 23 | public async Task PostWithJsonAsync(string url, object content, CancellationToken token) 24 | { 25 | var request = new RestRequest(url); 26 | request.AddHeader("Content-type", "application/json"); 27 | request.AddJsonBody(content); 28 | request.Method = Method.Post; 29 | return await _client.PostAsync(request, token); 30 | } 31 | 32 | public async Task PostWithQueryAsync(string url, Dictionary query, CancellationToken token = default) 33 | { 34 | var request = new RestRequest(url) { Method = Method.Post }; 35 | request.AddQueryParameters(query); 36 | return await _client.PostAsync(request, token); 37 | } 38 | 39 | private async Task> ExecuteAsync(RestRequest request, CancellationToken token) 40 | { 41 | return await _client.ExecuteAsync(request, token); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/Scripts/EnterRecaptchaCallBackScript.js: -------------------------------------------------------------------------------- 1 | const result = (function () { 2 | if (typeof (___grecaptcha_cfg) !== 'undefined') { 3 | let cs = []; 4 | for (let id in ___grecaptcha_cfg.clients) { 5 | cs.push(id); 6 | } 7 | let res = cs.map(cid => { 8 | for (let p in ___grecaptcha_cfg.clients[cid]) { 9 | let c = {}; 10 | cid >= 10000 ? c.version = 'V3' : c.version = 'V2'; 11 | let path = "___grecaptcha_cfg.clients[" + cid + "]." + p; 12 | let pp = eval(path); 13 | if (typeof pp === 'object') { 14 | for (let s in pp) { 15 | let subpath = "___grecaptcha_cfg.clients[" + cid + "]." + p + "." + s; 16 | let sp = eval(subpath); 17 | if (sp && typeof sp === 'object' && sp.hasOwnProperty('sitekey') && sp.hasOwnProperty('size')) { 18 | c.sitekey = eval(subpath + '.sitekey'); 19 | let cb = eval(subpath + '.callback'); 20 | if (cb == null) { 21 | c.callback = null; 22 | c.function = null; 23 | } 24 | else { 25 | c.callback = subpath + '.callback'; 26 | cb != c.callback ? c.function = cb : c.function = null; 27 | } 28 | } 29 | } 30 | } 31 | return c; 32 | } 33 | }); 34 | return (res)[0]; 35 | } else { 36 | return (null); 37 | } 38 | })() 39 | 40 | if (typeof (result.function) == 'function') { 41 | result.function(value) 42 | } 43 | 44 | else { 45 | eval(result.function).call(window, value); 46 | } 47 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Plugins/Recaptcha/readme.md: -------------------------------------------------------------------------------- 1 | ## Recaptcha plugin 2 | 3 | Solves visible captcha automatically with one single line of code! Can handle captcha with button and callback. 4 | 5 | ## Usage 6 | 7 | ```c# 8 | 9 | // Initialize recaptcha plugin with AntiCaptchaProvider 10 | var recaptchaPlugin = new RecaptchaPlugin(new AntiCaptcha("MyToken")); 11 | var browser = await new PuppeteerExtra().Use(recaptchaPlugin).LaunchAsync(new LaunchOptions() 12 | { 13 | Headless = true 14 | }); 15 | 16 | var page = await browser.NewPageAsync(); 17 | await page.GoToAsync("https://patrickhlauke.github.io/recaptcha/"); 18 | // Solves captcha in page! 19 | await recaptchaPlugin.SolveCaptchaAsync(page); 20 | 21 | ``` 22 | 23 | #### Providers 24 | 25 | 🤖 [AntiCaptcha](https://anti-captcha.com/mainpage) 26 | 27 | 👾 [2captcha](https://2captcha.com/ru) 28 | 29 | You can use your own provider implements IRecaptcha provider interface who should return g-recaptcha-responce. 30 | -------------------------------------------------------------------------------- /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 = new List(); 14 | 15 | public PuppeteerExtra Use(PuppeteerExtraPlugin plugin) 16 | { 17 | _plugins.Add(plugin); 18 | ResolveDependencies(plugin); 19 | plugin.OnPluginRegistered(); 20 | return this; 21 | } 22 | 23 | public async Task LaunchAsync(LaunchOptions options) 24 | { 25 | _plugins.ForEach(e => e.BeforeLaunch(options)); 26 | var browser = await Puppeteer.LaunchAsync(options); 27 | _plugins.ForEach(e => e.AfterLaunch(browser)); 28 | await OnStart(new BrowserStartContext() 29 | { 30 | StartType = StartType.Launch, 31 | IsHeadless = options.Headless 32 | }, browser); 33 | return browser; 34 | } 35 | 36 | public async Task ConnectAsync(ConnectOptions options) 37 | { 38 | _plugins.ForEach(e => e.BeforeConnect(options)); 39 | var browser = await Puppeteer.ConnectAsync(options); 40 | _plugins.ForEach(e => e.AfterConnect(browser)); 41 | await OnStart(new BrowserStartContext() 42 | { 43 | StartType = StartType.Connect 44 | }, browser); 45 | return browser; 46 | } 47 | 48 | public T GetPlugin() where T : PuppeteerExtraPlugin 49 | { 50 | return (T) _plugins.FirstOrDefault(e => e.GetType() == typeof(T)); 51 | } 52 | 53 | private async Task OnStart(BrowserStartContext context, IBrowser browser) 54 | { 55 | OrderPlugins(); 56 | CheckPluginRequirements(context); 57 | await Register(browser); 58 | } 59 | 60 | private void ResolveDependencies(PuppeteerExtraPlugin plugin) 61 | { 62 | var dependencies = plugin.GetDependencies()?.ToList(); 63 | if (dependencies is null || !dependencies.Any()) 64 | return; 65 | 66 | foreach (var puppeteerExtraPlugin in dependencies) 67 | { 68 | Use(puppeteerExtraPlugin); 69 | 70 | var plugDependencies = puppeteerExtraPlugin.GetDependencies()?.ToList(); 71 | 72 | if (plugDependencies != null && plugDependencies.Any()) 73 | plugDependencies.ForEach(ResolveDependencies); 74 | } 75 | } 76 | 77 | private void OrderPlugins() 78 | { 79 | _plugins = _plugins.OrderBy(e => e.Requirements?.Contains(PluginRequirements.RunLast)).ToList(); 80 | } 81 | 82 | private void CheckPluginRequirements(BrowserStartContext context) 83 | { 84 | foreach (var puppeteerExtraPlugin in _plugins) 85 | { 86 | if (puppeteerExtraPlugin.Requirements is null) 87 | continue; 88 | foreach (var requirement in puppeteerExtraPlugin.Requirements) 89 | { 90 | switch (context.StartType) 91 | { 92 | case StartType.Launch when requirement == PluginRequirements.HeadFul && context.IsHeadless: 93 | throw new NotSupportedException( 94 | $"Plugin - {puppeteerExtraPlugin.Name} is not supported in headless mode"); 95 | case StartType.Connect when requirement == PluginRequirements.Launch: 96 | throw new NotSupportedException( 97 | $"Plugin - {puppeteerExtraPlugin.Name} doesn't support connect"); 98 | } 99 | } 100 | } 101 | } 102 | 103 | private async Task Register(IBrowser browser) 104 | { 105 | var pages = await browser.PagesAsync(); 106 | 107 | browser.TargetCreated += async (sender, args) => 108 | { 109 | _plugins.ForEach(e => e.OnTargetCreated(args.Target)); 110 | if (args.Target.Type == TargetType.Page) 111 | { 112 | var page = await args.Target.PageAsync(); 113 | _plugins.ForEach(async e => await e.OnPageCreated(page)); 114 | } 115 | }; 116 | 117 | foreach (var puppeteerExtraPlugin in _plugins) 118 | { 119 | browser.TargetChanged += (sender, args) => puppeteerExtraPlugin.OnTargetChanged(args.Target); 120 | browser.TargetDestroyed += (sender, args) => puppeteerExtraPlugin.OnTargetDestroyed(args.Target); 121 | browser.Disconnected += (sender, args) => puppeteerExtraPlugin.OnDisconnected(); 122 | browser.Closed += (sender, args) => puppeteerExtraPlugin.OnClose(); 123 | foreach (var page in pages) 124 | { 125 | await puppeteerExtraPlugin.OnPageCreated(page); 126 | } 127 | } 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /PuppeteerExtraSharp/PuppeteerExtraSharp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | latest 6 | 1.3.1 7 | https://github.com/Overmiind/Puppeteer-sharp-extra 8 | git 9 | puppeteer-extra recaptcha browser-automation browser-extension browser puppeteer netcore netcore31 stealth-client browser-testing c# 10 | https://github.com/Overmiind/Puppeteer-sharp-extra 11 | MIT 12 | 945328fb-3e7e-4518-99f8-ec578bf688b1 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/README.md: -------------------------------------------------------------------------------- 1 | # PuppeteerExtraSharp 2 | 3 | [![NuGet Badge](https://buildstats.info/nuget/PuppeteerExtraSharp)](https://www.nuget.org/packages/PuppeteerExtraSharp) 4 | 5 | Puppeteer extra sharp is a .NET port of the [Node.js library](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra) 6 | ## Quickstart 7 | 8 | ```c# 9 | // Initialization plugin builder 10 | var extra = new PuppeteerExtra(); 11 | 12 | // Use stealth plugin 13 | extra.Use(new StealthPlugin()); 14 | 15 | // Launch the puppeteer browser with plugins 16 | var browser = await extra.LaunchAsync(new LaunchOptions() 17 | { 18 | Headless = false 19 | }); 20 | 21 | // Create a new page 22 | var page = await browser.NewPageAsync(); 23 | 24 | await page.GoToAsync("http://google.com"); 25 | 26 | // Wait 2 second 27 | await page.WaitForTimeoutAsync(2000); 28 | 29 | // Take the screenshot 30 | await page.ScreenshotAsync("extra.png"); 31 | ``` 32 | ## Plugin list 33 | 34 | 🏴 [Puppeteer stealth plugin](https://github.com/Overmiind/PuppeteerExtraSharp/tree/master/Plugins/ExtraStealth) 35 | - Applies various evasion techniques to make detection of headless puppeteer harder. 36 | 37 | 📃 [Puppeteer anonymize UA plugin](https://github.com/Overmiind/PuppeteerExtraSharp/tree/master/Plugins/AnonymizeUa) 38 | - Anonymizes the user-agent on all pages. 39 | 40 | 💀[Puppeteer recaptcha plugin](https://github.com/Overmiind/PuppeteerExtraSharp/tree/master/Plugins/Recaptcha) 41 | - Solves recaptcha automatically 42 | 43 | 44 | 45 | ✋**More plugins will be soon** 46 | ## API 47 | 48 | #### Use(IPuppeteerExtraPlugin) 49 | 50 | Adds a new plugin to plugins list and register it. 51 | - Returns the same instance of puppeteer extra 52 | - Parameters: instance of IPuppeteerExtraPlugin interface 53 | ```c# 54 | var puppeteerExtra = new PuppeteerExtra().Use(new AnonymizeUaPlugin()).Use(new StealthPlugin()); 55 | ``` 56 | 57 | #### LaunchAsync(LaunchOptions) 58 | 59 | - Return the new puppeteer browser instance with launch options 60 | 61 | ```c# 62 | var browser = new PuppeteerExtra().LaunchAsync(new LaunchOptions()); 63 | ``` 64 | 65 | #### ConnectAsync(ConnectOptions) 66 | - Connect to the exiting browser with connect options 67 | ```c# 68 | var browser = new PuppeteerExtra().ConnectAsync(new ConnectOptions()); 69 | ``` 70 | 71 | #### GetPlugin() 72 | - Get plugin from plugin list by type 73 | ```c# 74 | var stealthPlugin = puppeteerExtra.GetPlugin(); 75 | ``` 76 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Utils/ResoursesReader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | 4 | namespace PuppeteerExtraSharp.Utils 5 | { 6 | internal static class ResourcesReader 7 | { 8 | public static string ReadFile(string path, Assembly customAssemly = null) 9 | { 10 | var assembly = customAssemly ?? 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 | } 18 | } 19 | -------------------------------------------------------------------------------- /PuppeteerExtraSharp/Utils/RestHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using RestSharp; 3 | 4 | namespace PuppeteerExtraSharp.Utils 5 | { 6 | public static class RestHelper 7 | { 8 | public static RestRequest AddQueryParameters(this RestRequest request, Dictionary parameters) 9 | { 10 | foreach (var parameter in parameters) 11 | { 12 | request.AddQueryParameter(parameter.Key, parameter.Value); 13 | } 14 | 15 | return request; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PuppeteerExtraSharp 2 | 3 | [![NuGet Badge](https://buildstats.info/nuget/PuppeteerExtraSharp)](https://www.nuget.org/packages/PuppeteerExtraSharp) 4 | 5 | Puppeteer extra sharp is a .NET port of the [Node.js library](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra) 6 | ## Quickstart 7 | 8 | ```c# 9 | // Initialization plugin builder 10 | var extra = new PuppeteerExtra(); 11 | 12 | // Use stealth plugin 13 | extra.Use(new StealthPlugin()); 14 | 15 | // Launch the puppeteer browser with plugins 16 | var browser = await extra.LaunchAsync(new LaunchOptions() 17 | { 18 | Headless = false 19 | }); 20 | 21 | // Create a new page 22 | var page = await browser.NewPageAsync(); 23 | 24 | await page.GoToAsync("http://google.com"); 25 | 26 | // Wait 2 second 27 | await page.WaitForTimeoutAsync(2000); 28 | 29 | // Take the screenshot 30 | await page.ScreenshotAsync("extra.png"); 31 | ``` 32 | ## Plugin list 33 | 34 | 🏴 [Puppeteer stealth plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/ExtraStealth) 35 | - Applies various evasion techniques to make detection of headless puppeteer harder. 36 | 37 | 📃 [Puppeteer anonymize UA plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/AnonymizeUa) 38 | - Anonymizes the user-agent on all pages. 39 | 40 | 💀[Puppeteer recaptcha plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/Recaptcha) 41 | - Solves recaptcha automatically 42 | 43 | 🔧[Puppeteer block resources plugin](https://github.com/Overmiind/Puppeteer-sharp-extra/tree/master/PuppeteerExtraSharp/Plugins/BlockResources) 44 | - Blocks images, documents etc. 45 | 46 | 47 | ✋**More plugins coming soon** 48 | ## API 49 | 50 | #### Use(IPuppeteerExtraPlugin) 51 | 52 | Adds a new plugin to plugins list and register it. 53 | - Returns the same instance of puppeteer extra 54 | - Parameters: instance of IPuppeteerExtraPlugin interface 55 | ```c# 56 | var puppeteerExtra = new PuppeteerExtra().Use(new AnonymizeUaPlugin()).Use(new StealthPlugin()); 57 | ``` 58 | 59 | #### LaunchAsync(LaunchOptions) 60 | 61 | - Return the new puppeteer browser instance with launch options 62 | 63 | ```c# 64 | var browser = new PuppeteerExtra().LaunchAsync(new LaunchOptions()); 65 | ``` 66 | 67 | #### ConnectAsync(ConnectOptions) 68 | - Connect to the exiting browser with connect options 69 | ```c# 70 | var browser = new PuppeteerExtra().ConnectAsync(new ConnectOptions()); 71 | ``` 72 | 73 | #### GetPlugin() 74 | - Get plugin from plugin list by type 75 | ```c# 76 | var stealthPlugin = puppeteerExtra.GetPlugin(); 77 | ``` 78 | -------------------------------------------------------------------------------- /Tests/BlockResourcesTests/BlockResourcesPluginTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.BlockResources; 3 | using PuppeteerSharp; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.BlockResourcesTests 7 | { 8 | public class BlockResourcesPluginTests : BrowserDefault 9 | { 10 | public BlockResourcesPluginTests() 11 | { 12 | 13 | } 14 | 15 | [Fact] 16 | public void ShouldAddsToListOfRules() 17 | { 18 | var plugin = new BlockResourcesPlugin(); 19 | var rule = plugin.AddRule(builder => builder.BlockedResources(ResourceType.Document)); 20 | Assert.NotEmpty(plugin.BlockResources); 21 | Assert.Contains(ResourceType.Document, plugin.BlockResources[0].ResourceType); 22 | } 23 | 24 | [Fact] 25 | public void RuleForResource() 26 | { 27 | var plugin = new BlockResourcesPlugin(); 28 | var rule = plugin.AddRule(builder => builder.BlockedResources(ResourceType.Document)); 29 | Assert.True(rule.IsResourcesBlocked(ResourceType.Document)); 30 | } 31 | 32 | 33 | [Fact] 34 | public async Task RuleForPage() 35 | { 36 | var plugin = new BlockResourcesPlugin(); 37 | var browser = await base.LaunchWithPluginAsync(plugin); 38 | var actualPage = (await browser.PagesAsync())[0]; 39 | var otherPage = await browser.NewPageAsync(); 40 | var rule = plugin.AddRule(builder => builder.BlockedResources(ResourceType.Document).OnlyForPage(actualPage)); 41 | 42 | Assert.True(rule.IsPageBlocked(actualPage)); 43 | Assert.False(rule.IsPageBlocked(otherPage)); 44 | } 45 | 46 | [InlineData("http://google.com")] 47 | [InlineData("http://google.kz")] 48 | [InlineData("https://googleeee.com")] 49 | [Theory] 50 | public void RuleForUrl(string site) 51 | { 52 | var plugin = new BlockResourcesPlugin(); 53 | var rule = plugin.AddRule(builder => builder.ForUrl("google")); 54 | 55 | Assert.True(rule.IsSiteBlocked(site)); 56 | } 57 | 58 | 59 | [Fact] 60 | public void ShouldRemoveRule() 61 | { 62 | var plugin = new BlockResourcesPlugin(); 63 | 64 | var actualRule = plugin.AddRule(builder => builder.BlockedResources(ResourceType.Font)); 65 | var otherRule = plugin.AddRule(builder => builder.BlockedResources(ResourceType.Document)); 66 | 67 | plugin.RemoveRule(actualRule); 68 | 69 | Assert.DoesNotContain(actualRule, plugin.BlockResources); 70 | Assert.Contains(otherRule, plugin.BlockResources); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/BrowserDefault.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using PuppeteerExtraSharp; 9 | using PuppeteerExtraSharp.Plugins; 10 | using PuppeteerSharp; 11 | 12 | namespace Extra.Tests 13 | { 14 | public abstract class BrowserDefault : IDisposable 15 | { 16 | private readonly List _launchedBrowsers = new List(); 17 | protected BrowserDefault() 18 | { 19 | } 20 | 21 | protected async Task LaunchAsync(LaunchOptions options = null) 22 | { 23 | //DownloadChromeIfNotExists(); 24 | options ??= CreateDefaultOptions(); 25 | 26 | var browser = await Puppeteer.LaunchAsync(options); 27 | _launchedBrowsers.Add(browser); 28 | return browser; 29 | } 30 | 31 | protected async Task LaunchWithPluginAsync(PuppeteerExtraPlugin plugin, LaunchOptions options = null) 32 | { 33 | var extra = new PuppeteerExtra().Use(plugin); 34 | //DownloadChromeIfNotExists(); 35 | options ??= CreateDefaultOptions(); 36 | 37 | var browser = await extra.LaunchAsync(options); 38 | _launchedBrowsers.Add(browser); 39 | return browser; 40 | } 41 | 42 | protected async Task LaunchAndGetPage(PuppeteerExtraPlugin plugin = null) 43 | { 44 | IBrowser browser = null; 45 | if (plugin != null) 46 | browser = await LaunchWithPluginAsync(plugin); 47 | else 48 | browser = await LaunchAsync(); 49 | 50 | var page = (await browser.PagesAsync())[0]; 51 | 52 | return page; 53 | } 54 | 55 | 56 | private async void DownloadChromeIfNotExists() 57 | { 58 | if (File.Exists(Constants.PathToChrome)) 59 | return; 60 | 61 | await new BrowserFetcher(new BrowserFetcherOptions() 62 | { 63 | Path = Constants.PathToChrome 64 | }).DownloadAsync(BrowserFetcher.DefaultChromiumRevision); 65 | } 66 | 67 | protected LaunchOptions CreateDefaultOptions() 68 | { 69 | return new LaunchOptions() 70 | { 71 | ExecutablePath = Constants.PathToChrome, 72 | Headless = Constants.Headless 73 | }; 74 | } 75 | 76 | public void Dispose() 77 | { 78 | foreach (var launchedBrowser in _launchedBrowsers) 79 | { 80 | launchedBrowser.CloseAsync().Wait(); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Extra.Tests 6 | { 7 | public static class Constants 8 | { 9 | public static readonly string PathToChrome = @"C:\Program Files\Google\Chrome\Application\chrome.exe"; 10 | public static readonly bool Headless = true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/Extra.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.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 | 38 | 39 | 40 | 41 | True 42 | True 43 | Resources.resx 44 | 45 | 46 | True 47 | True 48 | Resources.resx 49 | 50 | 51 | 52 | 53 | 54 | ResXFileCodeGenerator 55 | Resources.Designer.cs 56 | 57 | 58 | ResXFileCodeGenerator 59 | Resources.Designer.cs 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Tests/ExtraLaunchTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha; 4 | using Xunit; 5 | 6 | namespace Extra.Tests 7 | { 8 | public class ExtraLaunchTest: BrowserDefault 9 | { 10 | [Fact] 11 | public async void ShouldReturnOkPage() 12 | { 13 | var browser = await this.LaunchAsync(); 14 | var page = await browser.NewPageAsync(); 15 | var response = await page.GoToAsync("http://google.com"); 16 | Assert.Equal(HttpStatusCode.OK, response.Status); 17 | await browser.CloseAsync(); 18 | } 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Extra.Tests.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Extra.Tests.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to "". 65 | /// 66 | internal static string AntiCaptchaKey { 67 | get { 68 | return ResourceManager.GetString("AntiCaptchaKey", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to "". 74 | /// 75 | internal static string TwoCaptchaKey { 76 | get { 77 | return ResourceManager.GetString("TwoCaptchaKey", resourceCulture); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | "" 122 | 123 | 124 | "" 125 | 126 | -------------------------------------------------------------------------------- /Tests/Recaptcha/AntiCaptcha/AntiCaptchaTests.cs: -------------------------------------------------------------------------------- 1 | using Extra.Tests.Properties; 2 | using PuppeteerExtraSharp.Plugins.Recaptcha; 3 | using PuppeteerSharp; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | using Task = System.Threading.Tasks.Task; 7 | 8 | namespace Extra.Tests.Recaptcha.AntiCaptcha 9 | { 10 | [Collection("Captcha")] 11 | public class AntiCaptchaTests : BrowserDefault 12 | { 13 | private readonly ITestOutputHelper _logger; 14 | 15 | public AntiCaptchaTests(ITestOutputHelper _logger) 16 | { 17 | this._logger = _logger; 18 | } 19 | 20 | [Fact] 21 | public async void ShouldThrowCaptchaExceptionWhenCaptchaNotFound() 22 | { 23 | var plugin = new RecaptchaPlugin(new PuppeteerExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha.AntiCaptcha(Resources.AntiCaptchaKey)); 24 | 25 | var browser = await LaunchWithPluginAsync(plugin); 26 | 27 | var page = await browser.NewPageAsync(); 28 | await page.GoToAsync("https://lessons.zennolab.com/ru/index"); 29 | var result = await plugin.SolveCaptchaAsync(page); 30 | Assert.NotNull(result.Exception); 31 | Assert.False(result.IsSuccess); 32 | //await browser.CloseAsync(); 33 | } 34 | 35 | [Fact] 36 | public async Task ShouldSolveCaptchaWithSubmitButton() 37 | { 38 | var plugin = new RecaptchaPlugin(new PuppeteerExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha.AntiCaptcha(Resources.AntiCaptchaKey)); 39 | var browser = await LaunchWithPluginAsync(plugin); 40 | 41 | var page = await browser.NewPageAsync(); 42 | await page.GoToAsync("https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=low"); 43 | var result = await plugin.SolveCaptchaAsync(page); 44 | 45 | Assert.Null(result.Exception); 46 | 47 | var button = await page.QuerySelectorAsync("input[type='submit']"); 48 | await button.ClickAsync(); 49 | 50 | await page.WaitForTimeoutAsync(1000); 51 | await CheckSuccessVerify(page); 52 | } 53 | 54 | [Fact] 55 | public async void ShouldSolveCaptchaWithCallback() 56 | { 57 | var plugin = new RecaptchaPlugin(new PuppeteerExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha.AntiCaptcha(Resources.AntiCaptchaKey)); 58 | var browser = await LaunchWithPluginAsync(plugin); 59 | var page = await browser.NewPageAsync(); 60 | await page.GoToAsync("https://lessons.zennolab.com/captchas/recaptcha/v2_nosubmit.php?level=low"); 61 | var result = await plugin.SolveCaptchaAsync(page); 62 | 63 | Assert.Null(result.Exception); 64 | 65 | await page.WaitForTimeoutAsync(1000); 66 | await CheckSuccessVerify(page); 67 | } 68 | 69 | private async Task CheckSuccessVerify(IPage page) 70 | { 71 | var successElement = await page.QuerySelectorAsync("div[id='main'] div[class='description'] h2"); 72 | var elementValue = await (await successElement.GetPropertyAsync("textContent")).JsonValueAsync(); 73 | Assert.NotNull(successElement); 74 | Assert.Equal("Успешная верификация!", elementValue); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/Recaptcha/RestClientTest/RestClientTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Xunit; 7 | using RestClient = PuppeteerExtraSharp.Plugins.Recaptcha.RestClient.RestClient; 8 | 9 | namespace Extra.Tests.Recaptcha.RestClientTest 10 | { 11 | [Collection("Captcha")] 12 | public class RestClientTests 13 | { 14 | [Fact] 15 | public async Task ShouldWorkPostWithJson() 16 | { 17 | var client = new RestClient("https://postman-echo.com"); 18 | var data = ("test", "123"); 19 | 20 | var result = await client.PostWithJsonAsync>("post", data, new CancellationToken()); 21 | 22 | var actual = JsonConvert.DeserializeObject<(string, string)>(result.First(e => e.Key == "json").Value); 23 | 24 | Assert.Equal(data, actual); 25 | } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/Recaptcha/TwoCaptcha/TwoCaptchaProviderTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Extra.Tests.Properties; 7 | using PuppeteerExtraSharp.Plugins.Recaptcha; 8 | using PuppeteerExtraSharp.Plugins.Recaptcha.Provider; 9 | using PuppeteerSharp; 10 | using Xunit; 11 | 12 | namespace Extra.Tests.Recaptcha.TwoCaptcha 13 | { 14 | [Collection("Captcha")] 15 | public class TwoCaptchaProviderTest: BrowserDefault 16 | { 17 | [Fact] 18 | public async Task ShouldResolveCaptchaInGooglePage() 19 | { 20 | var plugin = new RecaptchaPlugin(new PuppeteerExtraSharp.Plugins.Recaptcha.Provider._2Captcha.TwoCaptcha(Resources.TwoCaptchaKey)); 21 | var browser = await this.LaunchWithPluginAsync(plugin); 22 | 23 | var page = (await browser.PagesAsync())[0]; 24 | 25 | await page.GoToAsync("https://www.google.com/recaptcha/api2/demo"); 26 | 27 | await plugin.SolveCaptchaAsync(page); 28 | var button = await page.QuerySelectorAsync("input[id='recaptcha-demo-submit']"); 29 | await button.ClickAsync(); 30 | await page.WaitForNavigationAsync(); 31 | var successElement = await page.QuerySelectorAsync("div[class='recaptcha-success']"); 32 | 33 | Assert.NotNull(successElement); 34 | } 35 | 36 | [Fact] 37 | public async Task ShouldSolveInvisibleCaptcha() 38 | { 39 | var plugin = new RecaptchaPlugin(new PuppeteerExtraSharp.Plugins.Recaptcha.Provider._2Captcha.TwoCaptcha(Resources.TwoCaptchaKey)); 40 | var browser = await this.LaunchWithPluginAsync(plugin); 41 | 42 | var page = (await browser.PagesAsync())[0]; 43 | 44 | await page.GoToAsync("https://recaptcha-demo.appspot.com/recaptcha-v2-invisible.php"); 45 | 46 | var result = await plugin.SolveCaptchaAsync(page); 47 | 48 | Assert.Null(result.Exception); 49 | await page.WaitForNavigationAsync(); 50 | var elements = await page.QuerySelectorAllAsync("main h2"); 51 | 52 | Assert.Equal(2, elements.Length); 53 | 54 | var elementPropery = await (await elements[1].GetPropertyAsync("textContent")).JsonValueAsync(); 55 | Assert.Equal("Success!", elementPropery); 56 | 57 | } 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/ChromeAppTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Newtonsoft.Json.Linq; 3 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 4 | using PuppeteerSharp; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 8 | { 9 | public class ChromeAppTest : BrowserDefault 10 | { 11 | [Fact] 12 | public async Task ShouldWork() 13 | { 14 | var plugin = new ChromeApp(); 15 | 16 | var page = await LaunchAndGetPage(plugin); 17 | await page.GoToAsync("https://google.com"); 18 | 19 | var chrome = await page.EvaluateExpressionAsync("window.chrome"); 20 | Assert.NotNull(chrome); 21 | 22 | var app = await page.EvaluateExpressionAsync("chrome.app"); 23 | Assert.NotNull(app); 24 | 25 | var getIsInstalled = await page.EvaluateExpressionAsync("chrome.app.getIsInstalled()"); 26 | Assert.False(getIsInstalled); 27 | 28 | var installState = await page.EvaluateExpressionAsync("chrome.app.InstallState"); 29 | Assert.NotNull(installState); 30 | Assert.Equal("disabled", installState["DISABLED"]); 31 | Assert.Equal("installed", installState["INSTALLED"]); 32 | Assert.Equal("not_installed", installState["NOT_INSTALLED"]); 33 | 34 | var runningState = await page.EvaluateExpressionAsync("chrome.app.RunningState"); 35 | Assert.NotNull(runningState); 36 | Assert.Equal("cannot_run", runningState["CANNOT_RUN"]); 37 | Assert.Equal("ready_to_run", runningState["READY_TO_RUN"]); 38 | Assert.Equal("running", runningState["RUNNING"]); 39 | 40 | var details = await page.EvaluateExpressionAsync("chrome.app.getDetails()"); 41 | Assert.Null(details); 42 | 43 | var runningStateFunc = await page.EvaluateExpressionAsync("chrome.app.runningState()"); 44 | Assert.Equal("cannot_run", runningStateFunc); 45 | 46 | 47 | await Assert.ThrowsAsync(async () => await page.EvaluateExpressionAsync("chrome.app.getDetails('foo')")); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/ChromeSciTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 3 | using Xunit; 4 | 5 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 6 | { 7 | public class ChromeSciTest : BrowserDefault 8 | { 9 | [Fact] 10 | public async Task ShouldWork() 11 | { 12 | var plugin = new ChromeSci(); 13 | var page = await LaunchAndGetPage(plugin); 14 | await page.GoToAsync("https://google.com"); 15 | var sci = await page.EvaluateFunctionAsync(@"() => { 16 | const { timing } = window.performance 17 | const csi = window.chrome.csi() 18 | return { 19 | csi: { 20 | exists: window.chrome && 'csi' in window.chrome, 21 | toString: chrome.csi.toString() 22 | }, 23 | dataOK: { 24 | onloadT: csi.onloadT === timing.domContentLoadedEventEnd, 25 | startE: csi.startE === timing.navigationStart, 26 | pageT: Number.isInteger(csi.pageT), 27 | tran: Number.isInteger(csi.tran) 28 | } 29 | } 30 | }"); 31 | 32 | Assert.True(sci["csi"].Value("exists")); 33 | Assert.Equal("function () { [native code] }", sci["csi"]["toString"]); 34 | Assert.True(sci["dataOK"].Value("onloadT")); 35 | Assert.True(sci["dataOK"].Value("pageT")); 36 | Assert.True(sci["dataOK"].Value("startE")); 37 | Assert.True(sci["dataOK"].Value("tran")); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/CodecTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Extra.Tests.Utils; 3 | using Newtonsoft.Json.Linq; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 8 | { 9 | public class CodecTest : BrowserDefault 10 | { 11 | [Fact] 12 | public async Task SupportsCodec() 13 | { 14 | var plugin = new Codec(); 15 | var page = await LaunchAndGetPage(plugin); 16 | await page.GoToAsync("https://google.com"); 17 | var fingerPrint = await new FingerPrint().GetFingerPrint(page); 18 | 19 | Assert.Equal("probably", fingerPrint["videoCodecs"]["ogg"].Value()); 20 | Assert.Equal("probably", fingerPrint["videoCodecs"]["h264"].Value()); 21 | Assert.Equal("probably", fingerPrint["videoCodecs"]["webm"].Value()); 22 | 23 | Assert.Equal("probably", fingerPrint["audioCodecs"]["ogg"].Value()); 24 | Assert.Equal("probably", fingerPrint["audioCodecs"]["mp3"].Value()); 25 | Assert.Equal("probably", fingerPrint["audioCodecs"]["wav"].Value()); 26 | Assert.Equal("maybe", fingerPrint["audioCodecs"]["m4a"].Value()); 27 | Assert.Equal("probably", fingerPrint["audioCodecs"]["aac"].Value()); 28 | } 29 | 30 | [Fact] 31 | public async Task NotLeakModifications() 32 | { 33 | var plugin = new Codec(); 34 | var page = await LaunchAndGetPage(plugin); 35 | 36 | var canPlay = 37 | await page.EvaluateFunctionAsync( 38 | "() => document.createElement('audio').canPlayType.toString()"); 39 | Assert.Equal("function canPlayType() { [native code] }", canPlay); 40 | 41 | var canPlayHasName = await page.EvaluateFunctionAsync( 42 | "() => document.createElement('audio').canPlayType.name"); 43 | Assert.Equal("canPlayType", canPlayHasName); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/ContentWindowTest.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 ContentWindowTest : BrowserDefault 10 | { 11 | [Fact] 12 | public async Task IFrameShouldBeObject() 13 | { 14 | var plugin = new StealthPlugin(); 15 | 16 | var page = await LaunchAndGetPage(plugin); 17 | await page.GoToAsync("https://google.com"); 18 | 19 | var finger = await new FingerPrint().GetFingerPrint(page); 20 | 21 | Assert.Equal("object", finger["iframeChrome"]); 22 | } 23 | 24 | [Fact] 25 | public async Task ShouldNotBreakIFrames() 26 | { 27 | var plugin = new StealthPlugin(); 28 | 29 | var page = await LaunchAndGetPage(plugin); 30 | await page.GoToAsync("https://google.com"); 31 | 32 | const string testFuncReturnValue = "TESTSTRING"; 33 | 34 | await page.EvaluateFunctionAsync(@"(testFuncReturnValue) => { 35 | const { document } = window // eslint-disable-line 36 | const body = document.querySelector('body') 37 | const iframe = document.createElement('iframe') 38 | iframe.srcdoc = 'foobar' 39 | iframe.contentWindow.mySuperFunction = () => testFuncReturnValue 40 | body.appendChild(iframe) 41 | }", testFuncReturnValue); 42 | 43 | var result = 44 | await page.EvaluateExpressionAsync( 45 | "document.querySelector('iframe').contentWindow.mySuperFunction()"); 46 | 47 | Assert.Equal(testFuncReturnValue, result); 48 | } 49 | 50 | [Fact] 51 | public async Task ShouldCoverAllFrames() 52 | { 53 | var plugin = new StealthPlugin(); 54 | 55 | var page = await LaunchAndGetPage(plugin); 56 | await page.GoToAsync("https://google.com"); 57 | 58 | 59 | var basicFrame = await page.EvaluateFunctionAsync(@"() => { 60 | const el = document.createElement('iframe') 61 | document.body.appendChild(el) 62 | return typeof(el.contentWindow.chrome) 63 | }"); 64 | 65 | var sandboxSOIFrame = await page.EvaluateFunctionAsync(@"() => { 66 | const el = document.createElement('iframe') 67 | el.setAttribute('sandbox', 'allow-same-origin') 68 | document.body.appendChild(el) 69 | return typeof(el.contentWindow.chrome) 70 | }"); 71 | 72 | var sandboxSOASIFrame = await page.EvaluateFunctionAsync(@"() => { 73 | const el = document.createElement('iframe') 74 | el.setAttribute('sandbox', 'allow-same-origin allow-scripts') 75 | document.body.appendChild(el) 76 | return typeof(el.contentWindow.chrome) 77 | }"); 78 | 79 | var srcdocIFrame = await page.EvaluateFunctionAsync(@"() => { 80 | const el = document.createElement('iframe') 81 | el.srcdoc = 'blank page, boys.' 82 | document.body.appendChild(el) 83 | return typeof(el.contentWindow.chrome) 84 | }"); 85 | 86 | Assert.Equal("object", basicFrame); 87 | Assert.Equal("object", sandboxSOIFrame); 88 | Assert.Equal("object", sandboxSOASIFrame); 89 | Assert.Equal("object", srcdocIFrame); 90 | } 91 | 92 | 93 | [Fact] 94 | public async Task ShouldEmulateFeatures() 95 | { 96 | var plugin = new StealthPlugin(); 97 | 98 | var page = await LaunchAndGetPage(plugin); 99 | await page.GoToAsync("https://google.com"); 100 | 101 | var results = await page.EvaluateFunctionAsync(@"() => { 102 | const results = {} 103 | 104 | const iframe = document.createElement('iframe') 105 | iframe.srcdoc = 'page intentionally left blank' // Note: srcdoc 106 | document.body.appendChild(iframe) 107 | 108 | const basicIframe = document.createElement('iframe') 109 | basicIframe.src = 'data:text/plain;charset=utf-8,foobar' 110 | document.body.appendChild(iframe) 111 | 112 | results.descriptorsOK = (() => { 113 | // Verify iframe prototype isn't touched 114 | const descriptors = Object.getOwnPropertyDescriptors( 115 | HTMLIFrameElement.prototype 116 | ) 117 | const str = descriptors.contentWindow.get.toString() 118 | return str === `function get contentWindow() { [native code] }` 119 | })() 120 | 121 | results.noProxySignature = (() => { 122 | return iframe.srcdoc.toString.hasOwnProperty('[[IsRevoked]]') // eslint-disable-line 123 | })() 124 | 125 | results.doesExist = (() => { 126 | // Verify iframe isn't remapped to main window 127 | return !!iframe.contentWindow 128 | })() 129 | 130 | results.isNotAClone = (() => { 131 | // Verify iframe isn't remapped to main window 132 | return iframe.contentWindow !== window 133 | })() 134 | 135 | results.hasSameNumberOfPlugins = (() => { 136 | return ( 137 | window.navigator.plugins.length === 138 | iframe.contentWindow.navigator.plugins.length 139 | ) 140 | })() 141 | 142 | results.SelfIsNotWindow = (() => { 143 | return iframe.contentWindow.self !== window 144 | })() 145 | 146 | results.SelfIsNotWindowTop = (() => { 147 | return iframe.contentWindow.self !== window.top 148 | })() 149 | 150 | results.TopIsNotSame = (() => { 151 | return iframe.contentWindow.top !== iframe.contentWindow 152 | })() 153 | 154 | results.FrameElementMatches = (() => { 155 | return iframe.contentWindow.frameElement === iframe 156 | })() 157 | 158 | results.StackTraces = (() => { 159 | try { 160 | // eslint-disable-next-line 161 | document['createElement'](0) 162 | } catch (e) { 163 | return e.stack 164 | } 165 | return false 166 | })() 167 | 168 | return results 169 | }"); 170 | 171 | 172 | Assert.True(results.Value("descriptorsOK")); 173 | Assert.True(results.Value("doesExist")); 174 | Assert.True(results.Value("isNotAClone")); 175 | Assert.True(results.Value("hasSameNumberOfPlugins")); 176 | Assert.True(results.Value("SelfIsNotWindow")); 177 | Assert.True(results.Value("SelfIsNotWindowTop")); 178 | Assert.True(results.Value("TopIsNotSame")); 179 | 180 | Assert.DoesNotContain("at Object.apply", results["StackTraces"]); 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/LanguagesTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Utils; 4 | using Newtonsoft.Json.Linq; 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 LaunchAndGetPage(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["languages"].Select(e => e.Value())); 23 | } 24 | 25 | 26 | [Fact] 27 | public async Task ShouldWorkWithCustomSettings() 28 | { 29 | var plugin = new Languages(new StealthLanguagesOptions("fr-FR")); 30 | var page = await LaunchAndGetPage(plugin); 31 | 32 | await page.GoToAsync("https://google.com"); 33 | 34 | var fingerPrint = await new FingerPrint().GetFingerPrint(page); 35 | 36 | Assert.Contains("fr-FR", fingerPrint["languages"].Select(e => e.Value())); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/LoadTimesTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Newtonsoft.Json.Linq; 3 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 7 | { 8 | public class LoadTimesTest: BrowserDefault 9 | { 10 | [Fact] 11 | public async Task ShouldWork() 12 | { 13 | var stealthPlugin = new LoadTimes(); 14 | var page = await LaunchAndGetPage(stealthPlugin); 15 | 16 | await page.GoToAsync("https://google.com"); 17 | 18 | var loadTimes = await page.EvaluateFunctionAsync("() => window.chrome.loadTimes()"); 19 | 20 | Assert.NotNull(loadTimes); 21 | 22 | Assert.NotNull(loadTimes["connectionInfo"]); 23 | Assert.NotNull(loadTimes["npnNegotiatedProtocol"]); 24 | Assert.NotNull(loadTimes["wasAlternateProtocolAvailable"]); 25 | Assert.NotNull(loadTimes["wasAlternateProtocolAvailable"]); 26 | Assert.NotNull(loadTimes["wasFetchedViaSpdy"]); 27 | Assert.NotNull(loadTimes["wasNpnNegotiated"]); 28 | Assert.NotNull(loadTimes["firstPaintAfterLoadTime"]); 29 | Assert.NotNull(loadTimes["requestTime"]); 30 | Assert.NotNull(loadTimes["startLoadTime"]); 31 | Assert.NotNull(loadTimes["commitLoadTime"]); 32 | Assert.NotNull(loadTimes["finishDocumentLoadTime"]); 33 | Assert.NotNull(loadTimes["firstPaintTime"]); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/PermissionsTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Extra.Tests.Utils; 3 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 4 | using Xunit; 5 | 6 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 7 | { 8 | public class PermissionsTest: BrowserDefault 9 | { 10 | [Fact] 11 | public async Task ShouldBeDeniedInHttpSite() 12 | { 13 | var plugin = new Permissions(); 14 | var page = await LaunchAndGetPage(plugin); 15 | await page.GoToAsync("http://info.cern.ch/"); 16 | 17 | var finger = await new FingerPrint().GetFingerPrint(page); 18 | 19 | Assert.Equal("denied", finger["permissions"]["state"]); 20 | Assert.Equal("denied", finger["permissions"]["permission"]); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/PluginEvasionTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Utils; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 8 | { 9 | public class PluginEvasionTest: BrowserDefault 10 | { 11 | [Fact] 12 | public async Task ShouldNotHaveModifications() 13 | { 14 | var stealthPlugin = new PluginEvasion(); 15 | var page = await LaunchAndGetPage(stealthPlugin); 16 | 17 | await page.GoToAsync("https://google.com"); 18 | 19 | 20 | var fingerPrint = await new FingerPrint().GetFingerPrint(page); 21 | 22 | Assert.Equal(3, fingerPrint["plugins"].Count()); 23 | Assert.Equal(4, fingerPrint["mimeTypes"].Count()); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/PluginTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Extra.Tests.Utils; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 8 | { 9 | public class PluginTest : BrowserDefault 10 | { 11 | [Fact] 12 | public async Task HasMimeTypes() 13 | { 14 | var plugin = new PluginEvasion(); 15 | var page = await LaunchAndGetPage(plugin); 16 | await page.GoToAsync("https://google.com"); 17 | 18 | var finger = await new FingerPrint().GetFingerPrint(page); 19 | 20 | Assert.Equal(3, finger["plugins"].Count()); 21 | Assert.Equal(4, finger["mimeTypes"].Count()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/RuntimeTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Newtonsoft.Json.Linq; 3 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 4 | using PuppeteerSharp; 5 | using Xunit; 6 | 7 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 8 | { 9 | public class RuntimeTest : BrowserDefault 10 | { 11 | [Fact] 12 | public async Task ShouldAddConnectToChrome() 13 | { 14 | var plugin = new ChromeRuntime(); 15 | var page = await LaunchAndGetPage(plugin); 16 | 17 | await page.GoToAsync("https://google.com"); 18 | 19 | var runtime = await page.EvaluateExpressionAsync("chrome.runtime"); 20 | Assert.NotNull(runtime); 21 | 22 | var runtimeConnect = await page.EvaluateExpressionAsync("chrome.runtime.connect"); 23 | Assert.NotNull(runtimeConnect); 24 | 25 | var runtimeName = await page.EvaluateExpressionAsync("chrome.runtime.connect.name"); 26 | Assert.Equal("connect", runtimeName); 27 | 28 | var sendMessage = await page.EvaluateExpressionAsync("chrome.runtime.sendMessage.name"); 29 | Assert.NotNull(sendMessage); 30 | 31 | var sendMessageUndefined = await page.EvaluateExpressionAsync("chrome.runtime.sendMessage('nckgahadagoaajjgafhacjanaoiihapd', '') === undefined"); 32 | Assert.True(sendMessageUndefined); 33 | 34 | var validIdWorks = await page.EvaluateExpressionAsync("chrome.runtime.connect('nckgahadagoaajjgafhacjanaoiihapd') !== undefined"); 35 | Assert.True(validIdWorks); 36 | 37 | var nestedToString = await page.EvaluateExpressionAsync("chrome.runtime.connect('nckgahadagoaajjgafhacjanaoiihapd').onDisconnect.addListener + ''"); 38 | Assert.Equal("function addListener() { [native code] }", nestedToString); 39 | 40 | var noReturn = await page.EvaluateExpressionAsync("chrome.runtime.connect('nckgahadagoaajjgafhacjanaoiihapd').disconnect() === undefined"); 41 | Assert.True(noReturn); 42 | 43 | 44 | await AssertThrowsConnect(page, "chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.", ""); 45 | await AssertThrowsConnect(page, "No matching signature.", "", "", "", "", "", "", ""); 46 | await AssertThrowsConnect(page, "Invalid extension id: 'foo'", "", "foo"); 47 | await AssertThrowsConnect(page, "Error at property 'includeTlsChannelId': Invalid type: expected boolean, found number.", "", new { IncludeTlsChannelId = 777 }); 48 | } 49 | 50 | 51 | private async Task AssertThrowsConnect(IPage page, string error, params object[] args) 52 | { 53 | var start = 54 | "Evaluation failed: TypeError: Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): "; 55 | var ex = await Assert.ThrowsAsync(async () => 56 | await page.EvaluateFunctionAsync("(...args) => chrome.runtime.connect.call(...args)", args)); 57 | 58 | var currentError = start + error; 59 | Assert.StartsWith(currentError, ex.Message); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/SourceUrl/SourceUrlTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 5 | using PuppeteerSharp; 6 | using Xunit; 7 | 8 | namespace Extra.Tests.StealthPluginTests.EvasionsTests.SourceUrl 9 | { 10 | public class SourceUrlTest : BrowserDefault 11 | { 12 | private readonly string _pageUrl = Environment.CurrentDirectory + "\\StealthPluginTests\\EvasionsTests\\SourceUrl\\fixtures\\Test.html"; 13 | 14 | [Fact] 15 | public async Task ShouldWork() 16 | { 17 | var plugin = new PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions.SourceUrl(); 18 | 19 | var page = await LaunchAndGetPage(plugin); 20 | await page.GoToAsync(_pageUrl, WaitUntilNavigation.Load); 21 | 22 | await page.EvaluateExpressionAsync("document.querySelector('title')"); 23 | var result = await page.EvaluateExpressionAsync("document.querySelector('#result').innerText"); 24 | 25 | var result2 = await page.EvaluateFunctionAsync(@"() => { 26 | try { 27 | Function.prototype.toString.apply({}) 28 | } catch (err) { 29 | return err.stack 30 | }}"); 31 | 32 | Assert.Equal("PASS", result); 33 | Assert.DoesNotContain("__puppeteer_evaluation_script", result2); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.Evasions; 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 UserAgent(); 14 | var page = await LaunchAndGetPage(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.Value("userAgent")); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/VendorTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 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 Vendor(); 13 | var page = await LaunchAndGetPage(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 | -------------------------------------------------------------------------------- /Tests/StealthPluginTests/EvasionsTests/WebDriverTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PuppeteerExtraSharp.Plugins.ExtraStealth.Evasions; 3 | using Xunit; 4 | 5 | namespace Extra.Tests.StealthPluginTests.EvasionsTests 6 | { 7 | public class WebDriverTest : BrowserDefault 8 | { 9 | [Fact] 10 | public async Task ShouldWork() 11 | { 12 | var plugin = new WebDriver(); 13 | var page = await LaunchAndGetPage(plugin); 14 | await page.GoToAsync("https://google.com"); 15 | 16 | var driver = await page.EvaluateExpressionAsync("navigator.webdriver"); 17 | Assert.False(driver); 18 | } 19 | 20 | [Fact] 21 | public async Task WontKillOtherMethods() 22 | { 23 | var plugin = new WebDriver(); 24 | var page = await LaunchAndGetPage(plugin); 25 | await page.GoToAsync("https://google.com"); 26 | 27 | var data = await page.EvaluateExpressionAsync("navigator.javaEnabled()"); 28 | Assert.False(data); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/Script/fpCollect.js: -------------------------------------------------------------------------------- 1 | const fpCollect = function () { 2 | const UNKNOWN = 'unknown'; 3 | const ERROR = 'error'; 4 | 5 | const DEFAULT_ATTRIBUTES = { 6 | plugins: false, 7 | mimeTypes: false, 8 | userAgent: false, 9 | platform: false, 10 | languages: false, 11 | screen: false, 12 | touchScreen: false, 13 | videoCard: false, 14 | multimediaDevices: true, 15 | productSub: false, 16 | navigatorPrototype: false, 17 | etsl: false, 18 | screenDesc: false, 19 | phantomJS: false, 20 | nightmareJS: false, 21 | selenium: false, 22 | webDriver: false, 23 | webDriverValue: false, 24 | errorsGenerated: false, 25 | resOverflow: false, 26 | accelerometerUsed: true, 27 | screenMediaQuery: false, 28 | hasChrome: false, 29 | detailChrome: false, 30 | permissions: true, 31 | iframeChrome: false, 32 | debugTool: false, 33 | battery: false, 34 | deviceMemory: false, 35 | tpCanvas: true, 36 | sequentum: false, 37 | audioCodecs: false, 38 | videoCodecs: false 39 | }; 40 | 41 | const defaultAttributeToFunction = { 42 | userAgent: () => { 43 | return navigator.userAgent; 44 | }, 45 | plugins: () => { 46 | const pluginsRes = []; 47 | for (let i = 0; i < navigator.plugins.length; i++) { 48 | const plugin = navigator.plugins[i]; 49 | const pluginStr = [plugin.name, plugin.description, plugin.filename, plugin.version].join("::"); 50 | let mimeTypes = []; 51 | Object.keys(plugin).forEach((mt) => { 52 | mimeTypes.push([plugin[mt].type, plugin[mt].suffixes, plugin[mt].description].join("~")); 53 | }); 54 | mimeTypes = mimeTypes.join(","); 55 | pluginsRes.push(pluginStr + "__" + mimeTypes); 56 | } 57 | return pluginsRes; 58 | }, 59 | mimeTypes: () => { 60 | const mimeTypes = []; 61 | for (let i = 0; i < navigator.mimeTypes.length; i++) { 62 | let mt = navigator.mimeTypes[i]; 63 | mimeTypes.push([mt.description, mt.type, mt.suffixes].join("~~")); 64 | } 65 | return mimeTypes; 66 | }, 67 | platform: () => { 68 | if (navigator.platform) { 69 | return navigator.platform; 70 | } 71 | return UNKNOWN; 72 | }, 73 | languages: () => { 74 | if (navigator.languages) { 75 | return navigator.languages; 76 | } 77 | return UNKNOWN; 78 | }, 79 | screen: () => { 80 | return { 81 | wInnerHeight: window.innerHeight, 82 | wOuterHeight: window.outerHeight, 83 | wOuterWidth: window.outerWidth, 84 | wInnerWidth: window.innerWidth, 85 | wScreenX: window.screenX, 86 | wPageXOffset: window.pageXOffset, 87 | wPageYOffset: window.pageYOffset, 88 | cWidth: document.body.clientWidth, 89 | cHeight: document.body.clientHeight, 90 | sWidth: screen.width, 91 | sHeight: screen.height, 92 | sAvailWidth: screen.availWidth, 93 | sAvailHeight: screen.availHeight, 94 | sColorDepth: screen.colorDepth, 95 | sPixelDepth: screen.pixelDepth, 96 | wDevicePixelRatio: window.devicePixelRatio 97 | }; 98 | }, 99 | touchScreen: () => { 100 | let maxTouchPoints = 0; 101 | let touchEvent = false; 102 | if (typeof navigator.maxTouchPoints !== "undefined") { 103 | maxTouchPoints = navigator.maxTouchPoints; 104 | } else if (typeof navigator.msMaxTouchPoints !== "undefined") { 105 | maxTouchPoints = navigator.msMaxTouchPoints; 106 | } 107 | try { 108 | document.createEvent("TouchEvent"); 109 | touchEvent = true; 110 | } catch (_) { 111 | } 112 | 113 | const touchStart = "ontouchstart" in window; 114 | return [maxTouchPoints, touchEvent, touchStart]; 115 | }, 116 | videoCard: () => { 117 | try { 118 | const canvas = document.createElement('canvas'); 119 | const ctx = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); 120 | let webGLVendor, webGLRenderer; 121 | if (ctx.getSupportedExtensions().indexOf("WEBGL_debug_renderer_info") >= 0) { 122 | webGLVendor = ctx.getParameter(ctx.getExtension('WEBGL_debug_renderer_info').UNMASKED_VENDOR_WEBGL); 123 | webGLRenderer = ctx.getParameter(ctx.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL); 124 | } else { 125 | webGLVendor = "Not supported"; 126 | webGLRenderer = "Not supported"; 127 | } 128 | return [webGLVendor, webGLRenderer]; 129 | } catch (e) { 130 | return "Not supported;;;Not supported"; 131 | } 132 | }, 133 | multimediaDevices: () => { 134 | return new Promise((resolve) => { 135 | const deviceToCount = { 136 | "audiooutput": 0, 137 | "audioinput": 0, 138 | "videoinput": 0 139 | }; 140 | 141 | if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices 142 | && navigator.mediaDevices.enumerateDevices.name !== "bound reportBlock") { 143 | // bound reportBlock occurs with Brave 144 | navigator.mediaDevices.enumerateDevices().then((devices) => { 145 | if (typeof devices !== "undefined") { 146 | let name; 147 | for (let i = 0; i < devices.length; i++) { 148 | name = [devices[i].kind]; 149 | deviceToCount[name] = deviceToCount[name] + 1; 150 | } 151 | resolve({ 152 | speakers: deviceToCount.audiooutput, 153 | micros: deviceToCount.audioinput, 154 | webcams: deviceToCount.videoinput 155 | }); 156 | } else { 157 | resolve({ 158 | speakers: 0, 159 | micros: 0, 160 | webcams: 0 161 | }); 162 | } 163 | 164 | }); 165 | } else if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices 166 | && navigator.mediaDevices.enumerateDevices.name === "bound reportBlock") { 167 | resolve({ 168 | 'devicesBlockedByBrave': true 169 | }); 170 | } else { 171 | resolve({ 172 | speakers: 0, 173 | micros: 0, 174 | webcams: 0 175 | }); 176 | } 177 | }); 178 | }, 179 | productSub: () => { 180 | return navigator.productSub; 181 | }, 182 | navigatorPrototype: () => { 183 | let obj = window.navigator; 184 | const protoNavigator = []; 185 | do Object.getOwnPropertyNames(obj).forEach((name) => { 186 | protoNavigator.push(name); 187 | }); 188 | while (obj = Object.getPrototypeOf(obj)); 189 | 190 | let res; 191 | const finalProto = []; 192 | protoNavigator.forEach((prop) => { 193 | const objDesc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), prop); 194 | if (objDesc !== undefined) { 195 | if (objDesc.value !== undefined) { 196 | res = objDesc.value.toString(); 197 | } else if (objDesc.get !== undefined) { 198 | res = objDesc.get.toString(); 199 | } 200 | } 201 | else { 202 | res = ""; 203 | } 204 | finalProto.push(prop + "~~~" + res); 205 | }); 206 | return finalProto; 207 | }, 208 | etsl: () => { 209 | return eval.toString().length; 210 | }, 211 | screenDesc: () => { 212 | try { 213 | return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(screen), "width").get.toString(); 214 | } catch (e) { 215 | return ERROR; 216 | } 217 | }, 218 | nightmareJS: () => { 219 | return !!window.__nightmare; 220 | }, 221 | phantomJS: () => { 222 | return [ 223 | 'callPhantom' in window, 224 | '_phantom' in window, 225 | 'phantom' in window 226 | ]; 227 | }, 228 | selenium: () => { 229 | return [ 230 | 'webdriver' in window, 231 | '_Selenium_IDE_Recorder' in window, 232 | 'callSelenium' in window, 233 | '_selenium' in window, 234 | '__webdriver_script_fn' in document, 235 | '__driver_evaluate' in document, 236 | '__webdriver_evaluate' in document, 237 | '__selenium_evaluate' in document, 238 | '__fxdriver_evaluate' in document, 239 | '__driver_unwrapped' in document, 240 | '__webdriver_unwrapped' in document, 241 | '__selenium_unwrapped' in document, 242 | '__fxdriver_unwrapped' in document, 243 | '__webdriver_script_func' in document, 244 | document.documentElement.getAttribute("selenium") !== null, 245 | document.documentElement.getAttribute("webdriver") !== null, 246 | document.documentElement.getAttribute("driver") !== null 247 | ]; 248 | }, 249 | webDriver: () => { 250 | return 'webdriver' in navigator; 251 | }, 252 | webDriverValue: () => { 253 | return navigator.webdriver; 254 | }, 255 | errorsGenerated: () => { 256 | const errors = []; 257 | try { 258 | azeaze + 3; 259 | } catch (e) { 260 | errors.push(e.message); 261 | errors.push(e.fileName); 262 | errors.push(e.lineNumber); 263 | errors.push(e.description); 264 | errors.push(e.number); 265 | errors.push(e.columnNumber); 266 | try { 267 | errors.push(e.toSource().toString()); 268 | } catch (e) { 269 | errors.push(undefined); 270 | } 271 | } 272 | 273 | try { 274 | new WebSocket('itsgonnafail'); 275 | } catch (e) { 276 | errors.push(e.message); 277 | } 278 | return errors; 279 | }, 280 | resOverflow: () => { 281 | let depth = 0; 282 | let errorMessage = ''; 283 | let errorName = ''; 284 | let errorStacklength = 0; 285 | 286 | function iWillBetrayYouWithMyLongName() { 287 | try { 288 | depth++; 289 | iWillBetrayYouWithMyLongName(); 290 | } catch (e) { 291 | errorMessage = e.message; 292 | errorName = e.name; 293 | errorStacklength = e.stack.toString().length; 294 | } 295 | } 296 | 297 | iWillBetrayYouWithMyLongName(); 298 | return { 299 | depth: depth, 300 | errorMessage: errorMessage, 301 | errorName: errorName, 302 | errorStacklength: errorStacklength 303 | } 304 | 305 | }, 306 | accelerometerUsed: () => { 307 | return new Promise((resolve) => { 308 | window.ondevicemotion = event => { 309 | if (event.accelerationIncludingGravity.x !== null) { 310 | return resolve(true); 311 | } 312 | }; 313 | 314 | setTimeout(() => { 315 | return resolve(false); 316 | }, 300); 317 | }); 318 | }, 319 | screenMediaQuery: () => { 320 | return window.matchMedia('(min-width: ' + (window.innerWidth - 1) + 'px)').matches; 321 | }, 322 | hasChrome: () => { 323 | return !!window.chrome; 324 | }, 325 | detailChrome: () => { 326 | if (!window.chrome) return UNKNOWN; 327 | 328 | const res = {}; 329 | 330 | try { 331 | ["webstore", "runtime", "app", "csi", "loadTimes"].forEach((property) => { 332 | res[property] = window.chrome[property].constructor.toString().length; 333 | }); 334 | } catch (e) { 335 | res.properties = UNKNOWN; 336 | } 337 | 338 | try { 339 | window.chrome.runtime.connect(''); 340 | } catch (e) { 341 | res.connect = e.message.length; 342 | } 343 | try { 344 | window.chrome.runtime.sendMessage(); 345 | } catch (e) { 346 | res.sendMessage = e.message.length; 347 | } 348 | 349 | return res; 350 | }, 351 | permissions: () => { 352 | return new Promise((resolve) => { 353 | navigator.permissions.query({ name: 'notifications' }).then((val) => { 354 | resolve({ 355 | state: val.state, 356 | permission: Notification.permission 357 | }) 358 | }); 359 | }) 360 | }, 361 | iframeChrome: () => { 362 | const iframe = document.createElement('iframe'); 363 | iframe.srcdoc = 'blank page'; 364 | document.body.appendChild(iframe); 365 | 366 | const result = typeof iframe.contentWindow.chrome; 367 | iframe.remove(); 368 | 369 | return result; 370 | }, 371 | debugTool: () => { 372 | let cpt = 0; 373 | const regexp = /./; 374 | regexp.toString = () => { 375 | cpt++; 376 | return 'spooky'; 377 | }; 378 | console.debug(regexp); 379 | return cpt > 1; 380 | }, 381 | battery: () => { 382 | return 'getBattery' in window.navigator; 383 | }, 384 | deviceMemory: () => { 385 | return navigator.deviceMemory || 0; 386 | }, 387 | tpCanvas: () => { 388 | return new Promise((resolve) => { 389 | try { 390 | const img = new Image(); 391 | const canvasCtx = document.createElement('canvas').getContext('2d'); 392 | img.onload = () => { 393 | canvasCtx.drawImage(img, 0, 0); 394 | resolve(canvasCtx.getImageData(0, 0, 1, 1).data); 395 | }; 396 | 397 | img.onerror = () => { 398 | resolve(ERROR); 399 | }; 400 | img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; 401 | } catch (e) { 402 | resolve(ERROR); 403 | } 404 | }); 405 | }, 406 | sequentum: () => { 407 | return window.external && window.external.toString && window.external.toString().indexOf('Sequentum') > -1; 408 | }, 409 | audioCodecs: () => { 410 | const audioElt = document.createElement("audio"); 411 | 412 | if (audioElt.canPlayType) { 413 | return { 414 | ogg: audioElt.canPlayType('audio/ogg; codecs="vorbis"'), 415 | mp3: audioElt.canPlayType('audio/mpeg;'), 416 | wav: audioElt.canPlayType('audio/wav; codecs="1"'), 417 | m4a: audioElt.canPlayType('audio/x-m4a;'), 418 | aac: audioElt.canPlayType('audio/aac;'), 419 | } 420 | } 421 | return { 422 | ogg: UNKNOWN, 423 | mp3: UNKNOWN, 424 | wav: UNKNOWN, 425 | m4a: UNKNOWN, 426 | aac: UNKNOWN 427 | }; 428 | }, 429 | videoCodecs: () => { 430 | const videoElt = document.createElement("video"); 431 | 432 | if (videoElt.canPlayType) { 433 | return { 434 | ogg: videoElt.canPlayType('video/ogg; codecs="theora"'), 435 | h264: videoElt.canPlayType('video/mp4; codecs="avc1.42E01E"'), 436 | webm: videoElt.canPlayType('video/webm; codecs="vp8, vorbis"'), 437 | } 438 | } 439 | return { 440 | ogg: UNKNOWN, 441 | h264: UNKNOWN, 442 | webm: UNKNOWN, 443 | } 444 | } 445 | }; 446 | 447 | const addCustomFunction = function (name, isAsync, f) { 448 | DEFAULT_ATTRIBUTES[name] = isAsync; 449 | defaultAttributeToFunction[name] = f; 450 | }; 451 | 452 | const generateFingerprint = function () { 453 | return new Promise((resolve) => { 454 | const promises = []; 455 | const fingerprint = {}; 456 | Object.keys(DEFAULT_ATTRIBUTES).forEach((attribute) => { 457 | fingerprint[attribute] = {}; 458 | if (DEFAULT_ATTRIBUTES[attribute]) { 459 | promises.push(new Promise((resolve) => { 460 | defaultAttributeToFunction[attribute]().then((val) => { 461 | fingerprint[attribute] = val; 462 | return resolve(); 463 | }).catch((e) => { 464 | fingerprint[attribute] = { 465 | error: true, 466 | message: e.toString() 467 | }; 468 | return resolve(); 469 | }) 470 | })); 471 | } else { 472 | try { 473 | fingerprint[attribute] = defaultAttributeToFunction[attribute](); 474 | } catch (e) { 475 | fingerprint[attribute] = { 476 | error: true, 477 | message: e.toString() 478 | }; 479 | } 480 | } 481 | }); 482 | return Promise.all(promises).then(() => { 483 | return resolve(fingerprint); 484 | }); 485 | }); 486 | }; 487 | 488 | return { 489 | addCustomFunction: addCustomFunction, 490 | generateFingerprint: generateFingerprint, 491 | }; 492 | 493 | } -------------------------------------------------------------------------------- /Tests/StealthPluginTests/StealthPluginTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using PuppeteerExtraSharp.Plugins.ExtraStealth; 6 | using Xunit; 7 | 8 | namespace Extra.Tests.StealthPluginTests 9 | { 10 | public class StealthPluginTests: BrowserDefault 11 | { 12 | [Fact] 13 | public async Task ShouldBeNotDetected() 14 | { 15 | var plugin = new StealthPlugin(); 16 | var page = await LaunchAndGetPage(plugin); 17 | await page.GoToAsync("https://google.com"); 18 | 19 | var webdriver = await page.EvaluateExpressionAsync("navigator.webdriver"); 20 | Assert.False(webdriver); 21 | 22 | var headlessUserAgent = await page.EvaluateExpressionAsync("window.navigator.userAgent"); 23 | Assert.DoesNotContain("Headless", headlessUserAgent); 24 | 25 | var webDriverOverriden = 26 | await page.EvaluateExpressionAsync( 27 | "Object.getOwnPropertyDescriptor(navigator.__proto__, 'webdriver') !== undefined"); 28 | Assert.True(webDriverOverriden); 29 | 30 | var plugins = await page.EvaluateExpressionAsync("navigator.plugins.length"); 31 | Assert.NotEqual(0, plugins); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/Utils/FingerPrint.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json.Linq; 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 | --------------------------------------------------------------------------------