├── .gitattributes ├── .gitignore ├── BrowserStartContext.cs ├── Helpers ├── EnumHelpers.cs ├── OptionsHelpers.cs └── PageHelpers.cs ├── LICENSE.txt ├── Models ├── BrowserTypeEnum.cs ├── OverrideUserAgent.cs ├── PluginData.cs ├── PluginRequirement.cs ├── PluginType.cs ├── UserAgentBrand.cs └── UserAgentMetadata.cs ├── PlaywrightExtra.cs ├── PlaywrightExtraSharp.csproj ├── PlaywrightExtraSharp.sln ├── Plugins ├── AnonymizeUa │ └── AnonymizeUaExtraPlugin.cs ├── BlockResources │ ├── BlockResourcesExtraPlugin.cs │ └── BlockRule.cs ├── DisposeContext.cs ├── ExtraStealth │ ├── Evasions │ │ ├── ChromeApp.cs │ │ ├── ChromeRuntime.cs │ │ ├── ChromeSci.cs │ │ ├── Codec.cs │ │ ├── ContentWindow.cs │ │ ├── ExtraPluginEvasion.cs │ │ ├── HardwareConcurrency.cs │ │ ├── Languages.cs │ │ ├── LoadTimes.cs │ │ ├── OutDimensions.cs │ │ ├── Permissions.cs │ │ ├── SourceUrl.cs │ │ ├── StackTrace.cs │ │ ├── UserAgent.cs │ │ ├── Vendor.cs │ │ ├── WebDriver.cs │ │ └── WebGl.cs │ └── StealthExtraPlugin.cs ├── IPuppeteerExtraPluginOptions.cs ├── PlaywrightExtraPlugin.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 └── 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 ├── README.md └── Utils └── ResourceReader.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-detectable=false 2 | -------------------------------------------------------------------------------- /.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/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | 400 | .idea/ -------------------------------------------------------------------------------- /BrowserStartContext.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp; 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 | } -------------------------------------------------------------------------------- /Helpers/EnumHelpers.cs: -------------------------------------------------------------------------------- 1 | using PlaywrightExtraSharp.Models; 2 | 3 | namespace PlaywrightExtraSharp.Helpers; 4 | 5 | public static class EnumHelpers 6 | { 7 | public static string GetBrowserName(this BrowserTypeEnum browserTypeEnum) 8 | { 9 | return browserTypeEnum.ToString().ToLowerInvariant(); 10 | } 11 | } -------------------------------------------------------------------------------- /Helpers/OptionsHelpers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Helpers; 4 | 5 | public static class OptionsHelpers 6 | { 7 | public static BrowserNewContextOptions ToContextOptions(this BrowserNewPageOptions options, BrowserNewContextOptions? contextOptions = null) 8 | { 9 | return new BrowserNewContextOptions(contextOptions) 10 | { 11 | AcceptDownloads = options.AcceptDownloads, 12 | IgnoreHTTPSErrors = options.IgnoreHTTPSErrors, 13 | BypassCSP = options.BypassCSP, 14 | ViewportSize = options.ViewportSize, 15 | ScreenSize = options.ScreenSize, 16 | UserAgent = options.UserAgent, 17 | DeviceScaleFactor = options.DeviceScaleFactor, 18 | IsMobile = options.IsMobile, 19 | HasTouch = options.HasTouch, 20 | JavaScriptEnabled = options.JavaScriptEnabled, 21 | TimezoneId = options.TimezoneId, 22 | Geolocation = options.Geolocation, 23 | Locale = options.Locale, 24 | Permissions = options.Permissions, 25 | ExtraHTTPHeaders = options.ExtraHTTPHeaders, 26 | Offline = options.Offline, 27 | HttpCredentials = options.HttpCredentials, 28 | ColorScheme = options.ColorScheme, 29 | ReducedMotion = options.ReducedMotion, 30 | ForcedColors = options.ForcedColors, 31 | RecordHarPath = options.RecordHarPath, 32 | RecordHarContent = options.RecordHarContent, 33 | RecordHarMode = options.RecordHarMode, 34 | RecordHarOmitContent = options.RecordHarOmitContent, 35 | RecordHarUrlFilter = options.RecordHarUrlFilter, 36 | RecordHarUrlFilterString = options.RecordHarUrlFilterString, 37 | RecordHarUrlFilterRegex = options.RecordHarUrlFilterRegex, 38 | RecordVideoDir = options.RecordVideoDir, 39 | RecordVideoSize = options.RecordVideoSize, 40 | Proxy = options.Proxy, 41 | ServiceWorkers = options.ServiceWorkers, 42 | BaseURL = options.BaseURL, 43 | StrictSelectors = options.StrictSelectors 44 | }; 45 | } 46 | 47 | public static BrowserTypeLaunchPersistentContextOptions ToPersistentContextOptions(this BrowserNewPageOptions options, BrowserTypeLaunchPersistentContextOptions? persistentContextOptions) 48 | { 49 | return new BrowserTypeLaunchPersistentContextOptions(persistentContextOptions) 50 | { 51 | AcceptDownloads = options.AcceptDownloads, 52 | IgnoreHTTPSErrors = options.IgnoreHTTPSErrors, 53 | BypassCSP = options.BypassCSP, 54 | ViewportSize = options.ViewportSize, 55 | ScreenSize = options.ScreenSize, 56 | UserAgent = options.UserAgent, 57 | DeviceScaleFactor = options.DeviceScaleFactor, 58 | IsMobile = options.IsMobile, 59 | HasTouch = options.HasTouch, 60 | JavaScriptEnabled = options.JavaScriptEnabled, 61 | TimezoneId = options.TimezoneId, 62 | Geolocation = options.Geolocation, 63 | Locale = options.Locale, 64 | Permissions = options.Permissions, 65 | ExtraHTTPHeaders = options.ExtraHTTPHeaders, 66 | Offline = options.Offline, 67 | HttpCredentials = options.HttpCredentials, 68 | ColorScheme = options.ColorScheme, 69 | ReducedMotion = options.ReducedMotion, 70 | ForcedColors = options.ForcedColors, 71 | RecordHarPath = options.RecordHarPath, 72 | RecordHarContent = options.RecordHarContent, 73 | RecordHarMode = options.RecordHarMode, 74 | RecordHarOmitContent = options.RecordHarOmitContent, 75 | RecordHarUrlFilter = options.RecordHarUrlFilter, 76 | RecordHarUrlFilterString = options.RecordHarUrlFilterString, 77 | RecordHarUrlFilterRegex = options.RecordHarUrlFilterRegex, 78 | RecordVideoDir = options.RecordVideoDir, 79 | RecordVideoSize = options.RecordVideoSize, 80 | Proxy = options.Proxy, 81 | ServiceWorkers = options.ServiceWorkers, 82 | BaseURL = options.BaseURL, 83 | StrictSelectors = options.StrictSelectors 84 | }; 85 | } 86 | } -------------------------------------------------------------------------------- /Helpers/PageHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.Playwright; 3 | 4 | namespace PlaywrightExtraSharp.Helpers; 5 | 6 | public static class PageHelpers 7 | { 8 | /// 9 | /// Requests url and waits before network is idle or number of requests started equals finished 10 | /// 11 | /// Page from Playwright 12 | /// Url to request 13 | /// Page goto options 14 | /// Network idle time before request considered finished 15 | /// Some sites load only one script and after setTimeout loads everything else. This time between page request completed and before any new requests will spawn will be awaited 16 | /// 17 | public static async Task GotoAndWaitForIdleAsync( 18 | this IPage page, 19 | string url, 20 | PageGotoOptions? options = null, 21 | TimeSpan? idleTime = null, 22 | TimeSpan? timeBeforeScriptsActivate = null) 23 | { 24 | idleTime ??= TimeSpan.FromMilliseconds(500); 25 | timeBeforeScriptsActivate ??= TimeSpan.FromMilliseconds(500); 26 | 27 | var requestsStarted = 0; 28 | var requestsFinished = 0; 29 | 30 | var autoResetEvent = new AutoResetEvent(false); 31 | 32 | page.Request += PageOnRequestStarted; 33 | page.RequestFinished += PageOnRequestFinished; 34 | page.RequestFailed += PageOnRequestFinished; 35 | 36 | var retries = 10; 37 | IResponse? response = null; 38 | while (retries-- > 0) 39 | { 40 | try 41 | { 42 | response = await page.GotoAsync(url, options); 43 | await page.WaitForTimeoutAsync((float)timeBeforeScriptsActivate.Value.TotalMilliseconds); 44 | break; 45 | } 46 | catch (PlaywrightException pwe) 47 | { 48 | } 49 | } 50 | 51 | var lastRequestFinishedAt = DateTime.UtcNow; 52 | 53 | while (true) 54 | { 55 | autoResetEvent.WaitOne(100); 56 | 57 | if (requestsStarted == requestsFinished || DateTime.UtcNow - lastRequestFinishedAt >= idleTime) 58 | break; 59 | } 60 | 61 | page.Request -= PageOnRequestStarted; 62 | page.RequestFinished -= PageOnRequestFinished; 63 | page.RequestFailed -= PageOnRequestFinished; 64 | 65 | return response; 66 | 67 | void PageOnRequestStarted(object sender, IRequest request) 68 | { 69 | requestsStarted++; 70 | lastRequestFinishedAt = DateTime.UtcNow; 71 | } 72 | 73 | void PageOnRequestFinished(object sender, IRequest request) 74 | { 75 | requestsFinished++; 76 | lastRequestFinishedAt = DateTime.UtcNow; 77 | autoResetEvent.Set(); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Georgy Kazakov 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 | -------------------------------------------------------------------------------- /Models/BrowserTypeEnum.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Models; 2 | 3 | public enum BrowserTypeEnum 4 | { 5 | Chromium, 6 | Firefox, 7 | Webkit 8 | } -------------------------------------------------------------------------------- /Models/OverrideUserAgent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PlaywrightExtraSharp.Models; 4 | 5 | internal class OverrideUserAgent 6 | { 7 | public OverrideUserAgent(string userAgent, string platform, string acceptLanguage, 8 | UserAgentMetadata userAgentMetadata) 9 | { 10 | UserAgent = userAgent; 11 | Platform = platform; 12 | AcceptLanguage = acceptLanguage; 13 | UserAgentMetadata = userAgentMetadata; 14 | } 15 | 16 | [JsonPropertyName("userAgent")] public string UserAgent { get; } 17 | [JsonPropertyName("platform")] public string Platform { get; } 18 | [JsonPropertyName("acceptLanguage")] public string AcceptLanguage { get; } 19 | 20 | [JsonPropertyName("userAgentMetadata")] 21 | public UserAgentMetadata UserAgentMetadata { get; } 22 | } -------------------------------------------------------------------------------- /Models/PluginData.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Models; 2 | 3 | public sealed class PluginData 4 | { 5 | public PluginData(string name, string value) 6 | { 7 | Name = name; 8 | Value = value; 9 | } 10 | 11 | public string Name { get; } 12 | public string Value { get; } 13 | } -------------------------------------------------------------------------------- /Models/PluginRequirement.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Models; 2 | 3 | public enum PluginRequirement 4 | { 5 | Launch, 6 | Headful, 7 | DataFromPlugins, 8 | RunLast 9 | } -------------------------------------------------------------------------------- /Models/PluginType.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Models; 2 | 3 | public enum PluginType 4 | { 5 | Stealth 6 | } -------------------------------------------------------------------------------- /Models/UserAgentBrand.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PlaywrightExtraSharp.Models; 4 | 5 | internal class UserAgentBrand 6 | { 7 | public UserAgentBrand(string brand, string version) 8 | { 9 | Brand = brand; 10 | Version = version; 11 | } 12 | 13 | [JsonPropertyName("brand")] public string Brand { get; } 14 | [JsonPropertyName("version")] public string Version { get; } 15 | } -------------------------------------------------------------------------------- /Models/UserAgentMetadata.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PlaywrightExtraSharp.Models; 4 | 5 | internal class UserAgentMetadata 6 | { 7 | public UserAgentMetadata(List brands, string fullVersion, string platform, 8 | string platformVersion, string architecture, string model, bool mobile) 9 | { 10 | Brands = brands; 11 | FullVersion = fullVersion; 12 | Platform = platform; 13 | PlatformVersion = platformVersion; 14 | Architecture = architecture; 15 | Model = model; 16 | Mobile = mobile; 17 | } 18 | 19 | [JsonPropertyName("brands")] public List Brands { get; } 20 | [JsonPropertyName("fullVersion")] public string FullVersion { get; } 21 | [JsonPropertyName("platform")] public string Platform { get; } 22 | [JsonPropertyName("platformVersion")] public string PlatformVersion { get; } 23 | [JsonPropertyName("architecture")] public string Architecture { get; } 24 | [JsonPropertyName("model")] public string Model { get; } 25 | [JsonPropertyName("mobile")] public bool Mobile { get; } 26 | } -------------------------------------------------------------------------------- /PlaywrightExtra.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | using PlaywrightExtraSharp.Helpers; 3 | using PlaywrightExtraSharp.Models; 4 | using PlaywrightExtraSharp.Plugins; 5 | 6 | namespace PlaywrightExtraSharp; 7 | 8 | public class PlaywrightExtra : IBrowser, IDisposable 9 | { 10 | private readonly BrowserTypeEnum _browserTypeEnum; 11 | private IBrowser? _browser; 12 | private IBrowserContext? _browserContext; 13 | private IPlaywright? _playwright; 14 | private List _plugins = new(); 15 | private bool _persistContext; 16 | private bool _persistentLaunch; 17 | private string? _userDataDir; 18 | private BrowserNewContextOptions? _contextOptions; 19 | private BrowserTypeLaunchPersistentContextOptions? _persistentContextOptions; 20 | 21 | public PlaywrightExtra(BrowserTypeEnum browserTypeEnum) 22 | { 23 | _browserTypeEnum = browserTypeEnum; 24 | 25 | //Use(new DisposeContext()); 26 | } 27 | 28 | public PlaywrightExtra Use(PlaywrightExtraPlugin extraPlugin) 29 | { 30 | _plugins.Add(extraPlugin); 31 | ResolveDependencies(extraPlugin); 32 | extraPlugin.OnPluginRegistered(); 33 | 34 | return this; 35 | } 36 | 37 | public PlaywrightExtra Install() 38 | { 39 | var exitCode = Program.Main(new[] { "install", "--with-deps", _browserTypeEnum.GetBrowserName() }); 40 | if (exitCode != 0) throw new Exception($"Playwright exited with code {exitCode}"); 41 | 42 | return this; 43 | } 44 | 45 | public async Task LaunchPersistentAsync(BrowserTypeLaunchPersistentContextOptions? options = null, bool persistContext = true, string? userDataDir = null) 46 | { 47 | _persistContext = persistContext; 48 | _persistentLaunch = true; 49 | _userDataDir = userDataDir; 50 | _persistentContextOptions = options ?? new BrowserTypeLaunchPersistentContextOptions(); 51 | 52 | _playwright = await Playwright.CreateAsync(); 53 | 54 | await TriggerEventAndWait(x => x.BeforeLaunch(null, options)); 55 | 56 | if (_persistContext) 57 | { 58 | await TriggerEventAndWait(x => x.BeforeContext(null, options)); 59 | _browserContext = await _playwright[_browserTypeEnum.GetBrowserName()] 60 | .LaunchPersistentContextAsync(userDataDir ?? "", options); 61 | 62 | await TriggerEventAndWait(x => x.OnContextCreated(_browserContext, null, options)); 63 | 64 | _browser = _browserContext.Browser; 65 | 66 | await TriggerEventAndWait(x => x.OnBrowser(_browser, null)); 67 | 68 | _browserContext.Close += async (_, targetBrowserContext) => 69 | await TriggerEventAndWait(x => x.OnDisconnected(null, targetBrowserContext)); 70 | 71 | await TriggerEventAndWait(x => x.AfterLaunch(_browser)); 72 | } 73 | 74 | OrderPlugins(); 75 | CheckPluginRequirements(new BrowserStartContext 76 | { 77 | StartType = StartType.Launch, 78 | IsHeadless = options?.Headless ?? false 79 | }); 80 | 81 | return this; 82 | } 83 | 84 | public async Task LaunchAsync(BrowserTypeLaunchOptions? options = null, bool persistContext = true, BrowserNewContextOptions? contextOptions = null) 85 | { 86 | _persistContext = persistContext; 87 | _persistentLaunch = false; 88 | _contextOptions = contextOptions ?? new BrowserNewContextOptions(); 89 | 90 | await TriggerEventAndWait(x => x.BeforeLaunch(options, null)); 91 | 92 | _playwright = await Playwright.CreateAsync(); 93 | _browser = await _playwright[_browserTypeEnum.GetBrowserName()].LaunchAsync(options); 94 | await TriggerEventAndWait(x => x.OnBrowser(_browser, null)); 95 | 96 | _browser.Disconnected += async (_, targetBrowser) => 97 | await TriggerEventAndWait(x => x.OnDisconnected(_browser, null)); 98 | 99 | if (_persistContext) 100 | { 101 | await TriggerEventAndWait(x => x.BeforeContext(contextOptions, null)); 102 | _browserContext = await _browser.NewContextAsync(contextOptions); 103 | await TriggerEventAndWait(x => x.OnContextCreated(_browserContext, contextOptions, null)); 104 | 105 | _browserContext.Close += async (_, context) => 106 | await TriggerEventAndWait(x => x.OnDisconnected(null, context)); 107 | 108 | var blankPage = await _browserContext.NewPageAsync(); 109 | await blankPage.GotoAsync("about:blank"); 110 | } 111 | 112 | await TriggerEventAndWait(x => x.AfterLaunch(_browser)); 113 | 114 | OrderPlugins(); 115 | CheckPluginRequirements(new BrowserStartContext 116 | { 117 | StartType = StartType.Launch, 118 | IsHeadless = options?.Headless ?? false 119 | }); 120 | 121 | return this; 122 | } 123 | 124 | public async Task ConnectAsync(string wsEndpoint, BrowserTypeConnectOptions? options = null) 125 | { 126 | _persistContext = true; 127 | _persistentLaunch = false; 128 | 129 | await TriggerEventAndWait(x => x.BeforeConnect(options)); 130 | 131 | _playwright = await Playwright.CreateAsync(); 132 | _browser = (await _playwright[_browserTypeEnum.GetBrowserName()].ConnectAsync(wsEndpoint, options)); 133 | _browserContext = _browser.Contexts.First(); 134 | 135 | await TriggerEventAndWait(x => x.AfterConnect(_browser)); 136 | await TriggerEventAndWait(x => x.OnBrowser(_browser, null)); 137 | 138 | _browser.Disconnected += async (_, targetBrowser) => 139 | await TriggerEventAndWait(x => x.OnDisconnected(targetBrowser, null)); 140 | 141 | await TriggerEventAndWait(x => x.AfterLaunch(_browser)); 142 | 143 | OrderPlugins(); 144 | CheckPluginRequirements(new BrowserStartContext 145 | { 146 | StartType = StartType.Connect 147 | }); 148 | 149 | return this; 150 | } 151 | 152 | public async Task ConnectOverCDPAsync(string endpointURL, BrowserTypeConnectOverCDPOptions? options = null) 153 | { 154 | if (_browserTypeEnum != BrowserTypeEnum.Chromium) 155 | throw new InvalidOperationException("ConnectOverCDPAsync is only supported for chromium browser"); 156 | 157 | _persistContext = true; 158 | _persistentLaunch = false; 159 | 160 | await TriggerEventAndWait(x => x.BeforeConnect( 161 | options != null 162 | ? new BrowserTypeConnectOptions 163 | { 164 | Headers = options.Headers, 165 | Timeout = options.Timeout, 166 | SlowMo = options.SlowMo 167 | } 168 | : null)); 169 | 170 | _playwright = await Playwright.CreateAsync(); 171 | _browser = (await _playwright[_browserTypeEnum.GetBrowserName()].ConnectOverCDPAsync(endpointURL, options)); 172 | _browserContext = _browser.Contexts.First(); 173 | 174 | await TriggerEventAndWait(x => x.AfterConnect(_browser)); 175 | await TriggerEventAndWait(x => x.OnBrowser(_browser, null)); 176 | 177 | _browser.Disconnected += async (_, targetBrowser) => 178 | await TriggerEventAndWait(x => x.OnDisconnected(targetBrowser, null)); 179 | 180 | await TriggerEventAndWait(x => x.AfterLaunch(_browser)); 181 | 182 | OrderPlugins(); 183 | CheckPluginRequirements(new BrowserStartContext 184 | { 185 | StartType = StartType.Connect 186 | }); 187 | 188 | return this; 189 | } 190 | 191 | public Task NewPageAsync(BrowserNewPageOptions? options = default) 192 | => NewPageAsync(null, options); 193 | 194 | /// 195 | /// 196 | /// Creates a new page in a new browser context. Closing this page will close the context 197 | /// as well. 198 | /// 199 | /// 200 | /// /// User data dir to store cache 201 | /// Call options 202 | public async Task NewPageAsync(string? userDataDir = null, BrowserNewPageOptions? options = default) 203 | { 204 | options ??= new BrowserNewPageOptions(); 205 | 206 | IPage page = null!; 207 | 208 | if (_persistContext) 209 | { 210 | page = await _browserContext!.NewPageAsync(); 211 | page.Close += async (_, page1) => await TriggerEventAndWait(x => x.OnPageClose(page1)); 212 | } 213 | else 214 | { 215 | if (_persistentLaunch) 216 | { 217 | var persistentContextOptions = options.ToPersistentContextOptions(_persistentContextOptions); 218 | 219 | await TriggerEventAndWait(x => x.BeforeContext(null, persistentContextOptions)); 220 | var browserContext = await _playwright![_browserTypeEnum.GetBrowserName()] 221 | .LaunchPersistentContextAsync(_userDataDir ?? userDataDir ?? "", persistentContextOptions); 222 | await TriggerEventAndWait(x => x.OnContextCreated(browserContext, null, persistentContextOptions)); 223 | 224 | await TriggerEventAndWait(x => x.OnBrowser(null, browserContext)); 225 | 226 | browserContext.Close += async (_, targetBrowserContext) => 227 | await TriggerEventAndWait(x => x.OnDisconnected(null, targetBrowserContext)); 228 | 229 | _browser = browserContext.Browser; 230 | 231 | await TriggerEventAndWait(x => x.AfterLaunch(_browser)); 232 | 233 | page = await browserContext.NewPageAsync(); 234 | page.Close += async (_, page1) => 235 | { 236 | await TriggerEventAndWait(x => x.OnPageClose(page1)); 237 | await page1.Context.CloseAsync(); 238 | }; 239 | } 240 | else 241 | { 242 | var contextOptions = options.ToContextOptions(_contextOptions); 243 | 244 | await TriggerEventAndWait(x => x.BeforeContext(contextOptions, null)); 245 | var browserContext = await _browser.NewContextAsync(contextOptions); 246 | await TriggerEventAndWait(x => x.OnContextCreated(browserContext, contextOptions, null)); 247 | 248 | browserContext.Close += async (_, context) => 249 | await TriggerEventAndWait(x => x.OnDisconnected(null, context)); 250 | 251 | page = await browserContext.NewPageAsync(); 252 | page.Close += async (_, page1) => await TriggerEventAndWait(x => x.OnPageClose(page1)); 253 | } 254 | } 255 | 256 | await TriggerEventAndWait(x => x.OnPageCreated(page)); 257 | 258 | page.Request += async (_, request) => await TriggerEventAndWait(x => x.OnRequest(page, request)); 259 | 260 | return page; 261 | } 262 | 263 | public ValueTask DisposeAsync() 264 | { 265 | if(_browser is not null) 266 | return _browser.DisposeAsync(); 267 | return _browserContext.DisposeAsync(); 268 | } 269 | 270 | public async Task CloseAsync() 271 | { 272 | if(_browser is not null) 273 | await _browser.CloseAsync(); 274 | if(_browserContext is not null) 275 | await _browserContext.CloseAsync(); 276 | } 277 | 278 | public Task NewBrowserCDPSessionAsync() 279 | { 280 | return _browser.NewBrowserCDPSessionAsync(); 281 | } 282 | 283 | public Task NewContextAsync(BrowserNewContextOptions? options = null) 284 | { 285 | return _browser.NewContextAsync(); 286 | } 287 | 288 | public IBrowserType BrowserType => _browser.BrowserType; 289 | public IReadOnlyList Contexts => _browser?.Contexts ?? new []{ _browserContext! }; 290 | public bool IsConnected => _browser.IsConnected; 291 | public string Version => _browser.Version; 292 | 293 | event EventHandler? IBrowser.Disconnected 294 | { 295 | add => _browser.Disconnected += value; 296 | remove => _browser.Disconnected -= value; 297 | } 298 | 299 | public async void Dispose() 300 | { 301 | await _browser.DisposeAsync(); 302 | _playwright?.Dispose(); 303 | } 304 | 305 | private void ResolveDependencies(PlaywrightExtraPlugin extraPlugin) 306 | { 307 | foreach (var puppeteerExtraPlugin in extraPlugin.Dependencies) 308 | { 309 | Use(puppeteerExtraPlugin); 310 | puppeteerExtraPlugin.Dependencies.ToList().ForEach(ResolveDependencies); 311 | } 312 | } 313 | 314 | private void OrderPlugins() 315 | { 316 | _plugins = _plugins.OrderBy(e => e.Requirements.Contains(PluginRequirement.RunLast)).ToList(); 317 | } 318 | 319 | private void CheckPluginRequirements(BrowserStartContext context) 320 | { 321 | foreach (var puppeteerExtraPlugin in _plugins) 322 | foreach (var requirement in puppeteerExtraPlugin.Requirements) 323 | switch (context.StartType) 324 | { 325 | case StartType.Launch when requirement == PluginRequirement.Headful && context.IsHeadless: 326 | throw new NotSupportedException( 327 | $"Plugin - {puppeteerExtraPlugin.Name} is not supported in headless mode"); 328 | case StartType.Connect when requirement == PluginRequirement.Launch: 329 | throw new NotSupportedException( 330 | $"Plugin - {puppeteerExtraPlugin.Name} doesn't support connect"); 331 | } 332 | } 333 | 334 | private async Task TriggerEventAndWait(Func action) 335 | { 336 | try 337 | { 338 | await Task.WhenAll(_plugins.Select(action)); 339 | } 340 | catch (Exception ex) 341 | { 342 | Console.WriteLine(ex); 343 | } 344 | } 345 | } -------------------------------------------------------------------------------- /PlaywrightExtraSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;net7.0 4 | latest 5 | enable 6 | enable 7 | 8 | 1.0.7 9 | https://github.com/gshev/playwright-extra-sharp 10 | git 11 | playwright-extra recaptcha browser-automation browser-extension browser playwright netcore stealth-client browser-testing c# 12 | https://github.com/gshev/playwright-extra-sharp 13 | MIT 14 | true 15 | README.md 16 | Playwright version update to 1.39.0 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 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /PlaywrightExtraSharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlaywrightExtraSharp", "PlaywrightExtraSharp.csproj", "{29CBD15D-6095-45F0-827C-64E246A2F23D}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {29CBD15D-6095-45F0-827C-64E246A2F23D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {29CBD15D-6095-45F0-827C-64E246A2F23D}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {29CBD15D-6095-45F0-827C-64E246A2F23D}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {29CBD15D-6095-45F0-827C-64E246A2F23D}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /Plugins/AnonymizeUa/AnonymizeUaExtraPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Microsoft.Playwright; 3 | using PlaywrightExtraSharp.Models; 4 | 5 | namespace PlaywrightExtraSharp.Plugins.AnonymizeUa; 6 | 7 | public class AnonymizeUaExtraPlugin : PlaywrightExtraPlugin 8 | { 9 | private Func? _customAction; 10 | public override string Name => "anonymize-ua"; 11 | 12 | public override Func OnPageCreated => async page => 13 | { 14 | var ua = await page.EvaluateAsync("() => navigator.userAgent"); 15 | ua = ua.Replace("HeadlessChrome", "Chrome"); 16 | 17 | var uaVersion = ua.Contains("Chrome/") 18 | ? Regex.Match(ua, @"Chrome\/([\d|.]+)").Groups[1].Value 19 | : Regex.Match(ua, @"\/([\d|.]+)").Groups is { } groups 20 | ? groups[groups.Count - 1].Value 21 | : ""; 22 | 23 | var regex = new Regex(@"/\(([^)]+)\)/"); 24 | ua = regex.Replace(ua, "(Windows NT 10.0; Win64; x64)"); 25 | 26 | if (_customAction != null) 27 | ua = _customAction(ua); 28 | 29 | var platform = GetPlatform(ua); 30 | var brand = GetBrands(uaVersion); 31 | 32 | var isMobile = GetIsMobile(ua); 33 | var platformVersion = GetPlatformVersion(ua); 34 | var platformArch = GetPlatformArch(isMobile); 35 | var platformModel = GetPlatformModel(isMobile, ua); 36 | 37 | var overrideObject = new Dictionary 38 | { 39 | { "userAgent", ua }, 40 | { "platform", platform }, 41 | { "acceptLanguage", "en-US, en" }, 42 | { 43 | "userAgentMetadata", new Dictionary 44 | { 45 | { "brands", brand.ToArray() }, 46 | { "fullVersion", uaVersion }, 47 | { "platform", platform }, 48 | { "platformVersion", platformVersion }, 49 | { "architecture", platformArch }, 50 | { "model", platformModel }, 51 | { "mobile", isMobile } 52 | } 53 | } 54 | }; 55 | 56 | var session = await page.Context.NewCDPSessionAsync(page); 57 | await session.SendAsync("Network.setUserAgentOverride", overrideObject); 58 | }; 59 | 60 | public void CustomizeUa(Func? uaAction = default) 61 | { 62 | _customAction = uaAction; 63 | } 64 | 65 | private string GetPlatform(string ua) 66 | { 67 | if (ua.Contains("Mac OS X")) return "Mac OS X"; 68 | 69 | if (ua.Contains("Android")) return "Android"; 70 | 71 | if (ua.Contains("Linux")) return "Linux"; 72 | 73 | return "Windows"; 74 | } 75 | 76 | private string GetPlatformVersion(string ua) 77 | { 78 | if (ua.Contains("Mac OS X ")) return Regex.Match(ua, "Mac OS X ([^)]+)").Groups[1].Value; 79 | 80 | if (ua.Contains("Android ")) return Regex.Match(ua, "Android ([^;]+)").Groups[1].Value; 81 | 82 | if (ua.Contains("Windows ")) return Regex.Match(ua, @"Windows .*?([\d|.]+);").Groups[1].Value; 83 | 84 | return string.Empty; 85 | } 86 | 87 | private string GetPlatformArch(bool isMobile) 88 | { 89 | return isMobile ? string.Empty : "x86"; 90 | } 91 | 92 | protected string GetPlatformModel(bool isMobile, string ua) 93 | { 94 | return isMobile ? Regex.Match(ua, @"Android.*?;\s([^)]+)").Groups[1].Value : string.Empty; 95 | } 96 | 97 | private bool GetIsMobile(string ua) 98 | { 99 | return ua.Contains("Android"); 100 | } 101 | 102 | private List GetBrands(string uaVersion) 103 | { 104 | var seed = int.Parse(uaVersion.Split('.')[0]); 105 | 106 | var order = new List> 107 | { 108 | new() 109 | { 110 | 0, 1, 2 111 | }, 112 | new() 113 | { 114 | 0, 2, 1 115 | }, 116 | new() 117 | { 118 | 1, 0, 2 119 | }, 120 | new() 121 | { 122 | 1, 2, 0 123 | }, 124 | new() 125 | { 126 | 2, 0, 1 127 | }, 128 | new() 129 | { 130 | 2, 1, 0 131 | } 132 | }[seed % 6]; 133 | 134 | var escapedChars = new List 135 | { 136 | " ", 137 | " ", 138 | ";" 139 | }; 140 | 141 | var greaseyBrand = $"{escapedChars[order[0]]}Not{escapedChars[order[1]]}A{escapedChars[order[2]]}Brand"; 142 | var greasedBrandVersionList = new Dictionary(); 143 | 144 | greasedBrandVersionList.Add(order[0], new UserAgentBrand 145 | ( 146 | greaseyBrand, 147 | "99" 148 | )); 149 | greasedBrandVersionList.Add(order[1], new UserAgentBrand 150 | ( 151 | "Chromium", 152 | seed.ToString() 153 | )); 154 | 155 | greasedBrandVersionList.Add(order[2], new UserAgentBrand 156 | ( 157 | "Google Chrome", 158 | seed.ToString() 159 | )); 160 | 161 | return greasedBrandVersionList.OrderBy(e => e.Key).Select(e => e.Value).ToList(); 162 | } 163 | } -------------------------------------------------------------------------------- /Plugins/BlockResources/BlockResourcesExtraPlugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.BlockResources; 4 | 5 | public class BlockResourcesExtraPlugin : PlaywrightExtraPlugin 6 | { 7 | private readonly List _blockResources = new(); 8 | 9 | public BlockResourcesExtraPlugin(IEnumerable? blockResources = null, string? blockPattern = null) 10 | { 11 | if (blockResources != null) 12 | _blockResources.Add(new BlockRule(resourceType: blockResources)); 13 | if (blockPattern != null) 14 | _blockResources.Add(new BlockRule(sitePattern: blockPattern)); 15 | } 16 | 17 | public override string Name => "block-resources"; 18 | 19 | public override Func OnRequest => 20 | (page, request) => 21 | { 22 | var tcs = new TaskCompletionSource(); 23 | 24 | page.RouteAsync("**/*", async route => 25 | { 26 | if (_blockResources.Any(rule => rule.IsRequestBlocked(page, request))) 27 | await route.AbortAsync(); 28 | else 29 | await route.ContinueAsync(); 30 | 31 | tcs.SetResult(true); 32 | }); 33 | 34 | return tcs.Task; 35 | }; 36 | 37 | public override Func BeforeLaunch => (options1, options2) => 38 | { 39 | if (options1 != null) 40 | options1.Args = options1.Args?.Append("--site-per-process").Append("--disable-features=IsolateOrigins") 41 | .ToArray(); 42 | 43 | if (options2 != null) 44 | options2.Args = options2.Args?.Append("--site-per-process").Append("--disable-features=IsolateOrigins") 45 | .ToArray(); 46 | 47 | return Task.CompletedTask; 48 | }; 49 | 50 | private async Task OnPageRequest(object sender, IRequest request, TaskCompletionSource tcs) 51 | { 52 | if (sender is not IPage senderPage) 53 | { 54 | tcs.SetResult(true); 55 | return; 56 | } 57 | 58 | await senderPage.RouteAsync("**/*", async route => 59 | { 60 | if (_blockResources.Any(rule => rule.IsRequestBlocked(senderPage, request))) 61 | await route.AbortAsync(); 62 | else 63 | await route.ContinueAsync(); 64 | 65 | tcs.SetResult(true); 66 | }); 67 | } 68 | } -------------------------------------------------------------------------------- /Plugins/BlockResources/BlockRule.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Microsoft.Playwright; 3 | 4 | namespace PlaywrightExtraSharp.Plugins.BlockResources; 5 | 6 | public class BlockRule 7 | { 8 | private readonly IPage? _page; 9 | private readonly IEnumerable? _resourceType; 10 | private readonly string? _sitePattern; 11 | 12 | public BlockRule(IPage? page = null, string? sitePattern = null, IEnumerable? resourceType = null) 13 | { 14 | _page = page; 15 | _sitePattern = sitePattern; 16 | _resourceType = resourceType; 17 | } 18 | 19 | public bool IsRequestBlocked(IPage? fromPage, IRequest request) 20 | { 21 | return IsResourcesBlocked(request.ResourceType) || IsSiteBlocked(request.Url) || IsPageBlocked(fromPage); 22 | } 23 | 24 | 25 | private bool IsPageBlocked(IPage? page) 26 | { 27 | return _page != null && page.Equals(_page); 28 | } 29 | 30 | private bool IsSiteBlocked(string siteUrl) 31 | { 32 | return !string.IsNullOrWhiteSpace(_sitePattern) && Regex.IsMatch(siteUrl, _sitePattern!); 33 | } 34 | 35 | private bool IsResourcesBlocked(string resource) 36 | { 37 | return _resourceType?.Contains(resource) ?? false; 38 | } 39 | } -------------------------------------------------------------------------------- /Plugins/DisposeContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins; 4 | 5 | public class DisposeContext : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "dispose-context"; 8 | 9 | public override Func OnPageClose => page => 10 | { 11 | //Console.WriteLine("Disposing page"); 12 | return page.Context.DisposeAsync().AsTask(); 13 | }; 14 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/ChromeApp.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Microsoft.Playwright; 3 | 4 | [assembly: InternalsVisibleTo("Extra.Tests")] 5 | 6 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 7 | 8 | public class ChromeApp : PlaywrightExtraPlugin 9 | { 10 | public override string Name => "stealth-chromeApp"; 11 | 12 | public override Func OnPageCreated => page => EvaluateScript(page, "ChromeApp.js"); 13 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/ChromeRuntime.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class ChromeRuntime : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "stealth-runtime"; 8 | 9 | public override Func OnPageCreated => page => EvaluateScript(page, "Runtime.js"); 10 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/ChromeSci.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class ChromeSci : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "stealth_sci"; 8 | 9 | public override Func OnPageCreated => page => EvaluateScript(page, "SCI.js"); 10 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/Codec.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class Codec : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "stealth-codec"; 8 | 9 | public override Func OnPageCreated => page => EvaluateScript(page, "Codec.js"); 10 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/ContentWindow.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | using PlaywrightExtraSharp.Models; 3 | 4 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class ContentWindow : PlaywrightExtraPlugin 7 | { 8 | public override string Name => "Iframe.ContentWindow"; 9 | 10 | public override PluginRequirement[] Requirements => new[] 11 | { 12 | PluginRequirement.RunLast 13 | }; 14 | 15 | public override Func OnPageCreated => page => EvaluateScript(page, "ContentWindow.js"); 16 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/ExtraPluginEvasion.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class ExtraPluginEvasion : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "stealth-pluginEvasion"; 8 | 9 | public override Func OnPageCreated => page => EvaluateScript(page, "Plugin.js"); 10 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/HardwareConcurrency.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class HardwareConcurrency : PlaywrightExtraPlugin 6 | { 7 | private readonly StealthHardwareConcurrencyOptions _options; 8 | 9 | public HardwareConcurrency(StealthHardwareConcurrencyOptions? options = null) 10 | { 11 | _options = options ?? new StealthHardwareConcurrencyOptions(4); 12 | } 13 | 14 | public override string Name => "stealth/hardwareConcurrency"; 15 | 16 | public override Func OnPageCreated => 17 | page => EvaluateScript(page, "HardwareConcurrency.js", _options.Concurrency); 18 | } 19 | 20 | public class StealthHardwareConcurrencyOptions : IPlaywrightExtraPluginOptions 21 | { 22 | public StealthHardwareConcurrencyOptions(int concurrency) 23 | { 24 | Concurrency = concurrency; 25 | } 26 | 27 | public int Concurrency { get; } 28 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/Languages.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class Languages : PlaywrightExtraPlugin 6 | { 7 | private readonly StealthLanguagesOptions _options; 8 | 9 | public Languages(StealthLanguagesOptions? options = null) 10 | { 11 | _options = options ?? new StealthLanguagesOptions("en-US", "en"); 12 | } 13 | 14 | public override string Name => "stealth-language"; 15 | 16 | public override Func OnPageCreated => page => EvaluateScript(page, "Language.js", _options.Languages); 17 | } 18 | 19 | public class StealthLanguagesOptions : IPlaywrightExtraPluginOptions 20 | { 21 | public StealthLanguagesOptions(params string[] languages) 22 | { 23 | Languages = languages.Cast().ToArray(); 24 | } 25 | 26 | public object[] Languages { get; } 27 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/LoadTimes.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class LoadTimes : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "stealth-loadTimes"; 8 | 9 | public override Func OnPageCreated => page => EvaluateScript(page, "LoadTimes.js"); 10 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/OutDimensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class OutDimensions : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "stealth-dimensions"; 8 | 9 | public override Func OnPageCreated => page => EvaluateScript(page, "Outdimensions.js"); 10 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/Permissions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class Permissions : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "stealth-permissions"; 8 | 9 | public override Func OnPageCreated => page => EvaluateScript(page, "Permissions.js"); 10 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/SourceUrl.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Playwright; 3 | 4 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 5 | 6 | public class SourceUrl : PlaywrightExtraPlugin 7 | { 8 | public override string Name => "SourceUrl"; 9 | 10 | public override Func OnPageCreated => page => 11 | { 12 | var mainWordProperty = 13 | page.MainFrame.GetType().GetProperty("MainWorld", BindingFlags.NonPublic 14 | | BindingFlags.Public | BindingFlags.Instance); 15 | var mainWordGetters = mainWordProperty?.GetGetMethod(true); 16 | 17 | page.Load += async (_, _) => 18 | { 19 | var mainWord = mainWordGetters?.Invoke(page.MainFrame, null); 20 | var contextField = mainWord?.GetType() 21 | .GetField("_contextResolveTaskWrapper", BindingFlags.NonPublic | BindingFlags.Instance); 22 | if (contextField is not null) 23 | { 24 | var context = (TaskCompletionSource?)contextField.GetValue(mainWord); 25 | if (context?.Task == null) 26 | throw new InvalidOperationException(); 27 | var execution = await context.Task; 28 | var suffixField = execution.GetType() 29 | .GetField("_evaluationScriptSuffix", BindingFlags.NonPublic | BindingFlags.Instance); 30 | suffixField?.SetValue(execution, "//# sourceURL=''"); 31 | } 32 | }; 33 | return Task.CompletedTask; 34 | }; 35 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/StackTrace.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class StackTrace : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "stealth-stackTrace"; 8 | 9 | public override Func OnPageCreated => page => EvaluateScript(page, "Stacktrace.js"); 10 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/UserAgent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Microsoft.Playwright; 3 | using PlaywrightExtraSharp.Models; 4 | 5 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 6 | 7 | public class UserAgent : PlaywrightExtraPlugin 8 | { 9 | public override string Name => "stealth-userAgent"; 10 | 11 | public override Func OnPageCreated => async page => 12 | { 13 | var ua = await page.EvaluateAsync("() => navigator.userAgent"); 14 | ua = ua.Replace("HeadlessChrome/", "Chrome/"); 15 | var uaVersion = ua.Contains("Chrome/") 16 | ? Regex.Match(ua, @"Chrome\/([\d|.]+)").Groups[1].Value 17 | : Regex.Match(ua, @"\/([\d|.]+)").Groups is { } groups 18 | ? groups[groups.Count - 1].Value 19 | : ""; 20 | 21 | var platform = GetPlatform(ua); 22 | var brand = GetBrands(uaVersion); 23 | 24 | var isMobile = GetIsMobile(ua); 25 | var platformVersion = GetPlatformVersion(ua); 26 | var platformArch = GetPlatformArch(isMobile); 27 | var platformModel = GetPlatformModel(isMobile, ua); 28 | 29 | var overrideObject = new Dictionary 30 | { 31 | { "userAgent", ua }, 32 | { "platform", platform }, 33 | { "acceptLanguage", "en-US, en" }, 34 | { 35 | "userAgentMetadata", new Dictionary 36 | { 37 | { "brands", brand.ToArray() }, 38 | { "fullVersion", uaVersion }, 39 | { "platform", platform }, 40 | { "platformVersion", platformVersion }, 41 | { "architecture", platformArch }, 42 | { "model", platformModel }, 43 | { "mobile", isMobile } 44 | } 45 | } 46 | }; 47 | 48 | var session = await page.Context.NewCDPSessionAsync(page); 49 | await session.SendAsync("Network.setUserAgentOverride", overrideObject); 50 | }; 51 | 52 | private static string GetPlatform(string ua) 53 | { 54 | if (ua.Contains("Mac OS X")) return "Mac OS X"; 55 | 56 | if (ua.Contains("Android")) return "Android"; 57 | 58 | if (ua.Contains("Linux")) return "Linux"; 59 | 60 | return "Windows"; 61 | } 62 | 63 | private static string GetPlatformVersion(string ua) 64 | { 65 | if (ua.Contains("Mac OS X ")) return Regex.Match(ua, "Mac OS X ([^)]+)").Groups[1].Value; 66 | 67 | if (ua.Contains("Android ")) return Regex.Match(ua, "Android ([^;]+)").Groups[1].Value; 68 | 69 | if (ua.Contains("Windows ")) return Regex.Match(ua, @"Windows .*?([\d|.]+);").Groups[1].Value; 70 | 71 | return string.Empty; 72 | } 73 | 74 | private static string GetPlatformArch(bool isMobile) 75 | { 76 | return isMobile ? string.Empty : "x86"; 77 | } 78 | 79 | private static string GetPlatformModel(bool isMobile, string ua) 80 | { 81 | return isMobile ? Regex.Match(ua, @"Android.*?;\s([^)]+)").Groups[1].Value : string.Empty; 82 | } 83 | 84 | private static bool GetIsMobile(string ua) 85 | { 86 | return ua.Contains("Android"); 87 | } 88 | 89 | private static List GetBrands(string uaVersion) 90 | { 91 | var seed = int.Parse(uaVersion.Split('.')[0]); 92 | 93 | var order = new List> 94 | { 95 | new() 96 | { 97 | 0, 1, 2 98 | }, 99 | new() 100 | { 101 | 0, 2, 1 102 | }, 103 | new() 104 | { 105 | 1, 0, 2 106 | }, 107 | new() 108 | { 109 | 1, 2, 0 110 | }, 111 | new() 112 | { 113 | 2, 0, 1 114 | }, 115 | new() 116 | { 117 | 2, 1, 0 118 | } 119 | }[seed % 6]; 120 | 121 | var escapedChars = new List 122 | { 123 | " ", 124 | " ", 125 | ";" 126 | }; 127 | 128 | var greaseyBrand = $"{escapedChars[order[0]]}Not{escapedChars[order[1]]}A{escapedChars[order[2]]}Brand"; 129 | var greasedBrandVersionList = new Dictionary(); 130 | 131 | greasedBrandVersionList.Add(order[0], new UserAgentBrand 132 | ( 133 | greaseyBrand, 134 | "99" 135 | )); 136 | greasedBrandVersionList.Add(order[1], new UserAgentBrand 137 | ( 138 | "Chromium", 139 | seed.ToString() 140 | )); 141 | 142 | greasedBrandVersionList.Add(order[2], new UserAgentBrand 143 | ( 144 | "Google Chrome", 145 | seed.ToString() 146 | )); 147 | 148 | return greasedBrandVersionList.OrderBy(e => e.Key).Select(e => e.Value).ToList(); 149 | } 150 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/Vendor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class Vendor : PlaywrightExtraPlugin 6 | { 7 | private readonly StealthVendorSettings _settings; 8 | 9 | public Vendor(StealthVendorSettings? settings = null) 10 | { 11 | _settings = settings ?? new StealthVendorSettings("Google Inc."); 12 | } 13 | 14 | public override string Name => "stealth-vendor"; 15 | 16 | public override Func OnPageCreated => page => EvaluateScript(page, "Vendor.js", _settings.Vendor); 17 | } 18 | 19 | public class StealthVendorSettings : IPlaywrightExtraPluginOptions 20 | { 21 | public StealthVendorSettings(string vendor) 22 | { 23 | Vendor = vendor; 24 | } 25 | 26 | public string Vendor { get; } 27 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/WebDriver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class WebDriver : PlaywrightExtraPlugin 6 | { 7 | public override string Name => "stealth-webDriver"; 8 | 9 | public override Func OnPageCreated => page => EvaluateScript(page, "WebDriver.js"); 10 | 11 | public override Func BeforeLaunch => 12 | (options1, options2) => 13 | { 14 | var args = options1?.Args?.ToList() ?? options2?.Args?.ToList() ?? new List(); 15 | var idx = args.FindIndex(e => e.StartsWith("--disable-blink-features=")); 16 | if (idx != -1) 17 | { 18 | var arg = args[idx]; 19 | args[idx] = $"{arg}, AutomationControlled"; 20 | return Task.CompletedTask; 21 | } 22 | 23 | args.Add("--disable-blink-features=AutomationControlled"); 24 | 25 | if (options1 != null) options1.Args = args.ToArray(); 26 | if (options2 != null) options2.Args = args.ToArray(); 27 | return Task.CompletedTask; 28 | }; 29 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/Evasions/WebGl.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 4 | 5 | public class WebGl : PlaywrightExtraPlugin 6 | { 7 | private readonly StealthWebGLOptions _options; 8 | 9 | public WebGl(StealthWebGLOptions? options = null) 10 | { 11 | _options = options ?? new StealthWebGLOptions("Intel Inc.", "Intel Iris OpenGL Engine"); 12 | } 13 | 14 | public override string Name => "stealth-webGl"; 15 | 16 | public override Func OnPageCreated => 17 | page => EvaluateScript(page, "WebGL.js", _options.Vendor, _options.Renderer); 18 | } 19 | 20 | public class StealthWebGLOptions : IPlaywrightExtraPluginOptions 21 | { 22 | public StealthWebGLOptions(string vendor, string renderer) 23 | { 24 | Vendor = vendor; 25 | Renderer = renderer; 26 | } 27 | 28 | public string Vendor { get; } 29 | public string Renderer { get; } 30 | } -------------------------------------------------------------------------------- /Plugins/ExtraStealth/StealthExtraPlugin.cs: -------------------------------------------------------------------------------- 1 | using PlaywrightExtraSharp.Plugins.ExtraStealth.Evasions; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.ExtraStealth; 4 | 5 | public class StealthExtraPlugin : PlaywrightExtraPlugin 6 | { 7 | private readonly IPlaywrightExtraPluginOptions[] _options; 8 | private readonly List _standardEvasions; 9 | 10 | public StealthExtraPlugin(params IPlaywrightExtraPluginOptions[] options) 11 | { 12 | _options = options; 13 | _standardEvasions = GetStandardEvasions(); 14 | } 15 | 16 | public override string Name => "stealth"; 17 | 18 | public override PlaywrightExtraPlugin[] Dependencies => _standardEvasions.ToArray(); 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 ExtraPluginEvasion(), 36 | new StackTrace(), 37 | new HardwareConcurrency(GetOptionByType()), 38 | new ContentWindow() 39 | // playwright does not seems to have problem with SourceUrl 40 | // new SourceUrl() 41 | }; 42 | } 43 | 44 | private T? GetOptionByType() where T : IPlaywrightExtraPluginOptions 45 | { 46 | return _options.OfType().FirstOrDefault(); 47 | } 48 | 49 | public void RemoveEvasionByType() where T : PlaywrightExtraPlugin 50 | { 51 | _standardEvasions.RemoveAll(ev => ev is T); 52 | } 53 | } -------------------------------------------------------------------------------- /Plugins/IPuppeteerExtraPluginOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Plugins; 2 | 3 | public interface IPlaywrightExtraPluginOptions 4 | { 5 | } -------------------------------------------------------------------------------- /Plugins/PlaywrightExtraPlugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | using PlaywrightExtraSharp.Models; 3 | using PlaywrightExtraSharp.Utils; 4 | 5 | namespace PlaywrightExtraSharp.Plugins; 6 | 7 | public abstract class PlaywrightExtraPlugin 8 | { 9 | public abstract string Name { get; } 10 | public virtual PluginRequirement[] Requirements { get; set; } = Array.Empty(); 11 | public virtual PlaywrightExtraPlugin[] Dependencies { get; set; } = Array.Empty(); 12 | public virtual List ScriptDependencies { get; set; } = new() { "Utils.js" }; 13 | 14 | public virtual Func OnPluginRegistered { get; set; } = () => Task.CompletedTask; 15 | public virtual Func BeforeLaunch { get; set; } = (_,_) => Task.CompletedTask; 16 | public virtual Func AfterLaunch { get; set; } = _ => Task.CompletedTask; 17 | public virtual Func BeforeConnect { get; set; } = _ => Task.CompletedTask; 18 | public virtual Func AfterConnect { get; set; } = _ => Task.CompletedTask; 19 | public virtual Func OnBrowser { get; set; } = (_,_) => Task.CompletedTask; 20 | public virtual Func OnPageCreated { get; set; } = _ => Task.CompletedTask; 21 | public virtual Func OnPageClose { get; set; } = _ => Task.CompletedTask; 22 | public virtual Func OnRequest { get; set; } = (_, _) => Task.CompletedTask; 23 | public virtual Func OnDisconnected { get; set; } = (_,_) => Task.CompletedTask; 24 | 25 | public virtual Func BeforeContext { get; set; } = 26 | (_,_) => Task.CompletedTask; 27 | 28 | public virtual Func OnContextCreated { get; set; } = 29 | (_, _, _) => Task.CompletedTask; 30 | 31 | protected async Task EvaluateScript(IPage page, string scriptName, params object[] args) 32 | { 33 | var scriptList = ScriptDependencies.Select( 34 | x => ResourceReader.ReadFile($"{typeof(PlaywrightExtraPlugin).Namespace}.Scripts.{x}")).ToList(); 35 | 36 | scriptList.Add(ResourceReader.ReadFile($"{typeof(PlaywrightExtraPlugin).Namespace}.Scripts.{scriptName}")); 37 | 38 | if (!page.IsClosed) 39 | await page.EvaluateAsync(string.Join("\n", scriptList), args); 40 | } 41 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/CaptchaCfg.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Plugins.Recaptcha; 2 | 3 | public class CaptchaCfg 4 | { 5 | public CaptchaCfg(string callback) 6 | { 7 | Callback = callback; 8 | } 9 | 10 | public string Callback { get; } 11 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/CaptchaException.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Plugins.Recaptcha; 2 | 3 | public class CaptchaException : Exception 4 | { 5 | public CaptchaException(string pageUrl, string content) 6 | { 7 | PageUrl = pageUrl; 8 | Content = content; 9 | } 10 | 11 | public string PageUrl { get; set; } 12 | public string Content { get; set; } 13 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/CaptchaOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Plugins.Recaptcha; 2 | 3 | public class CaptchaOptions 4 | { 5 | public bool VisualFeedBack { get; set; } = false; 6 | public bool IsThrowException { get; set; } = false; 7 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/2Captcha/Models/TwoCaptchaRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.Provider._2Captcha.Models; 4 | 5 | internal class TwoCaptchaRequest 6 | { 7 | public TwoCaptchaRequest(string key) 8 | { 9 | Key = key; 10 | } 11 | 12 | [JsonPropertyName("key")] public string Key { get; } 13 | } 14 | 15 | internal class TwoCaptchaTask : TwoCaptchaRequest 16 | { 17 | public TwoCaptchaTask(string key, string method, string googleKey, string pageUrl) : base(key) 18 | { 19 | Method = method; 20 | GoogleKey = googleKey; 21 | PageUrl = pageUrl; 22 | } 23 | 24 | [JsonPropertyName("method")] public string Method { get; } = "userrecaptcha"; 25 | [JsonPropertyName("googlekey")] public string GoogleKey { get; } 26 | [JsonPropertyName("pageurl")] public string PageUrl { get; } 27 | } 28 | 29 | internal class TwoCaptchaRequestForResult : TwoCaptchaRequest 30 | { 31 | public TwoCaptchaRequestForResult(string key, string action, string id) : base(key) 32 | { 33 | Action = action; 34 | Id = id; 35 | } 36 | 37 | [JsonPropertyName("action")] public string Action { get; } = "get"; 38 | [JsonPropertyName("id")] public string Id { get; } 39 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/2Captcha/Models/TwoCaptchaResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.Provider._2Captcha.Models; 4 | 5 | internal class TwoCaptchaResponse 6 | { 7 | public TwoCaptchaResponse(int status, string request) 8 | { 9 | Status = status; 10 | Request = request; 11 | } 12 | 13 | [JsonPropertyName("status")] public int Status { get; } 14 | [JsonPropertyName("request")] public string Request { get; } 15 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/2Captcha/TwoCaptcha.cs: -------------------------------------------------------------------------------- 1 | using PlaywrightExtraSharp.Plugins.Recaptcha.Provider._2Captcha.Models; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.Provider._2Captcha; 4 | 5 | public class TwoCaptcha : IRecaptchaProvider 6 | { 7 | private readonly TwoCaptchaApi _api; 8 | private readonly ProviderOptions _options; 9 | 10 | public TwoCaptcha(string key, ProviderOptions? options = null) 11 | { 12 | _options = options ?? ProviderOptions.CreateDefaultOptions(); 13 | _api = new TwoCaptchaApi(key, _options); 14 | } 15 | 16 | public async Task GetSolution(string key, string pageUrl, string? proxyStr = null) 17 | { 18 | var task = await _api.CreateTaskAsync(key, pageUrl); 19 | 20 | ThrowErrorIfBadStatus(task); 21 | 22 | await Task.Delay(_options.StartTimeoutSeconds * 1000); 23 | 24 | var result = await _api.GetSolution(task!.Request); 25 | 26 | ThrowErrorIfBadStatus(result.Data); 27 | 28 | return result.Data?.Request; 29 | } 30 | 31 | private static void ThrowErrorIfBadStatus(TwoCaptchaResponse? response) 32 | { 33 | if (response == null) 34 | throw new HttpRequestException("Two captcha request ends with empty response"); 35 | if (response.Status != 1 || string.IsNullOrEmpty(response.Request)) 36 | throw new HttpRequestException( 37 | $"Two captcha request ends with error [{response.Status}] {response.Request}"); 38 | } 39 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/2Captcha/TwoCaptchaApi.cs: -------------------------------------------------------------------------------- 1 | using PlaywrightExtraSharp.Plugins.Recaptcha.Provider._2Captcha.Models; 2 | using PlaywrightExtraSharp.Plugins.Recaptcha.RestClient; 3 | using RestSharp; 4 | 5 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.Provider._2Captcha; 6 | 7 | internal class TwoCaptchaApi 8 | { 9 | private readonly RestClient.RestClient _client = new("https://rucaptcha.com"); 10 | private readonly ProviderOptions _options; 11 | private readonly string _userKey; 12 | 13 | public TwoCaptchaApi(string userKey, ProviderOptions options) 14 | { 15 | _userKey = userKey; 16 | _options = options; 17 | } 18 | 19 | public async Task CreateTaskAsync(string key, string pageUrl) 20 | { 21 | var result = await _client.PostWithQueryAsync("in.php", new Dictionary 22 | { 23 | ["key"] = _userKey, 24 | ["googlekey"] = key, 25 | ["pageurl"] = pageUrl, 26 | ["json"] = "1", 27 | ["method"] = "userrecaptcha" 28 | }); 29 | 30 | return result; 31 | } 32 | 33 | 34 | public async Task> GetSolution(string id) 35 | { 36 | var request = new RestRequest("res.php") { Method = Method.Post }; 37 | 38 | request.AddQueryParameter("id", id); 39 | request.AddQueryParameter("key", _userKey); 40 | request.AddQueryParameter("action", "get"); 41 | request.AddQueryParameter("json", "1"); 42 | 43 | var result = await _client.CreatePollingBuilder(request).TriesLimit(_options.PendingCount) 44 | .ActivatePollingAsync( 45 | response => response.Data?.Request == "CAPCHA_NOT_READY" 46 | ? PollingAction.ContinuePolling 47 | : PollingAction.Break); 48 | 49 | return result; 50 | } 51 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/AntiCaptcha/AntiCaptcha.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha; 2 | 3 | public class AntiCaptcha : IRecaptchaProvider 4 | { 5 | private readonly AntiCaptchaApi _api; 6 | private readonly ProviderOptions _options; 7 | 8 | public AntiCaptcha(string userKey, ProviderOptions? options = null) 9 | { 10 | _options = options ?? ProviderOptions.CreateDefaultOptions(); 11 | _api = new AntiCaptchaApi(userKey, _options); 12 | } 13 | 14 | public async Task GetSolution(string key, string pageUrl, string? proxyStr = null) 15 | { 16 | var task = await _api.CreateTaskAsync(pageUrl, key); 17 | 18 | if (task == null) 19 | throw new HttpRequestException("AntiCaptcha request failed"); 20 | 21 | await Task.Delay(_options.StartTimeoutSeconds * 1000); 22 | var result = await _api.PendingForResult(task.TaskId); 23 | 24 | if (result?.Status != "ready" || result.Solution is null || result.ErrorId != 0) 25 | throw new HttpRequestException($"AntiCaptcha request ends with error - {result?.ErrorId}"); 26 | 27 | return result.Solution.GRecaptchaResponse; 28 | } 29 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/AntiCaptcha/AntiCaptchaApi.cs: -------------------------------------------------------------------------------- 1 | using PlaywrightExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha.Models; 2 | using PlaywrightExtraSharp.Plugins.Recaptcha.RestClient; 3 | using RestSharp; 4 | 5 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha; 6 | 7 | public class AntiCaptchaApi 8 | { 9 | private readonly RestClient.RestClient _client = new("http://api.anti-captcha.com"); 10 | private readonly ProviderOptions _options; 11 | private readonly string _userKey; 12 | 13 | public AntiCaptchaApi(string userKey, ProviderOptions options) 14 | { 15 | _userKey = userKey; 16 | _options = options; 17 | } 18 | 19 | public Task CreateTaskAsync(string pageUrl, string key, CancellationToken token = default) 20 | { 21 | var content = new AntiCaptchaRequest 22 | ( 23 | _userKey, 24 | new AntiCaptchaTask 25 | ( 26 | "NoCaptchaTaskProxyless", 27 | pageUrl, 28 | key 29 | ) 30 | ); 31 | 32 | var result = _client.PostWithJsonAsync("createTask", content, token); 33 | return result; 34 | } 35 | 36 | 37 | public async Task PendingForResult(int taskId, CancellationToken token = default) 38 | { 39 | var content = new RequestForResultTask 40 | ( 41 | _userKey, 42 | taskId 43 | ); 44 | 45 | 46 | var request = new RestRequest("getTaskResult"); 47 | request.AddJsonBody(content); 48 | request.Method = Method.Post; 49 | 50 | var result = await _client.CreatePollingBuilder(request).TriesLimit(_options.PendingCount) 51 | .WithTimeoutSeconds(5).ActivatePollingAsync( 52 | response => 53 | { 54 | if (response.Data?.Status == "ready" || response.Data?.ErrorId != 0) 55 | return PollingAction.Break; 56 | 57 | return PollingAction.ContinuePolling; 58 | }); 59 | return result.Data; 60 | } 61 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/AntiCaptcha/Models/AntiCaptchaRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha.Models; 4 | 5 | public class AntiCaptchaRequest 6 | { 7 | public AntiCaptchaRequest(string clientKey, AntiCaptchaTask task) 8 | { 9 | ClientKey = clientKey; 10 | Task = task; 11 | } 12 | 13 | [JsonPropertyName("clientKey")] public string ClientKey { get; } 14 | 15 | [JsonPropertyName("task")] public AntiCaptchaTask Task { get; } 16 | } 17 | 18 | public class RequestForResultTask 19 | { 20 | public RequestForResultTask(string clientKey, int taskId) 21 | { 22 | ClientKey = clientKey; 23 | TaskId = taskId; 24 | } 25 | 26 | [JsonPropertyName("clientKey")] public string ClientKey { get; } 27 | 28 | [JsonPropertyName("taskId")] public int TaskId { get; } 29 | } 30 | 31 | public class AntiCaptchaTaskResult 32 | { 33 | public AntiCaptchaTaskResult(int errorId, int taskId) 34 | { 35 | ErrorId = errorId; 36 | TaskId = taskId; 37 | } 38 | 39 | [JsonPropertyName("errorId")] public int ErrorId { get; } 40 | 41 | [JsonPropertyName("taskId")] public int TaskId { get; } 42 | } 43 | 44 | public class AntiCaptchaTask 45 | { 46 | public AntiCaptchaTask(string type, string websiteUrl, string websiteKey) 47 | { 48 | Type = type; 49 | WebsiteUrl = websiteUrl; 50 | WebsiteKey = websiteKey; 51 | } 52 | 53 | [JsonPropertyName("type")] public string Type { get; } 54 | 55 | [JsonPropertyName("websiteURL")] public string WebsiteUrl { get; } 56 | 57 | [JsonPropertyName("websiteKey")] public string WebsiteKey { get; } 58 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/AntiCaptcha/Models/TaskResultModel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.Provider.AntiCaptcha.Models; 4 | 5 | public class Solution 6 | { 7 | public Solution(string gRecaptchaResponse) 8 | { 9 | GRecaptchaResponse = gRecaptchaResponse; 10 | } 11 | 12 | [JsonPropertyName("gRecaptchaResponse")] 13 | public string GRecaptchaResponse { get; } 14 | } 15 | 16 | public class TaskResultModel 17 | { 18 | public TaskResultModel(int errorId, string status, Solution solution, string cost, string ip, int createTime, 19 | int endTime, string solveCount) 20 | { 21 | ErrorId = errorId; 22 | Status = status; 23 | Solution = solution; 24 | Cost = cost; 25 | Ip = ip; 26 | CreateTime = createTime; 27 | EndTime = endTime; 28 | SolveCount = solveCount; 29 | } 30 | 31 | [JsonPropertyName("errorId")] public int ErrorId { get; } 32 | 33 | [JsonPropertyName("status")] public string Status { get; } 34 | 35 | [JsonPropertyName("solution")] public Solution Solution { get; } 36 | 37 | [JsonPropertyName("cost")] public string Cost { get; } 38 | 39 | [JsonPropertyName("ip")] public string Ip { get; } 40 | 41 | [JsonPropertyName("createTime")] public int CreateTime { get; } 42 | 43 | [JsonPropertyName("endTime")] public int EndTime { get; } 44 | 45 | [JsonPropertyName("solveCount")] public string SolveCount { get; } 46 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/IRecaptchaProvider.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.Provider; 2 | 3 | public interface IRecaptchaProvider 4 | { 5 | public Task GetSolution(string key, string pageUrl, string? proxyStr = null); 6 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Provider/ProviderOptions.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.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 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/RecapchaPlugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | using PlaywrightExtraSharp.Plugins.Recaptcha.Provider; 3 | 4 | namespace PlaywrightExtraSharp.Plugins.Recaptcha; 5 | 6 | public class RecaptchaExtraPlugin : PlaywrightExtraPlugin 7 | { 8 | private readonly Recaptcha _recaptcha; 9 | 10 | public RecaptchaExtraPlugin(IRecaptchaProvider provider, CaptchaOptions? opt = null) 11 | { 12 | _recaptcha = new Recaptcha(provider, opt ?? new CaptchaOptions()); 13 | } 14 | 15 | public override string Name => "recaptcha"; 16 | 17 | public override Func BeforeContext => 18 | (options1, options2) => 19 | { 20 | if(options1 != null) 21 | options1.BypassCSP = true; 22 | if(options2 != null) 23 | options2.BypassCSP = true; 24 | return Task.CompletedTask; 25 | }; 26 | 27 | public async Task SolveCaptchaAsync(IPage page) 28 | { 29 | return await _recaptcha.Solve(page); 30 | } 31 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/Recaptcha.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using Microsoft.Playwright; 3 | using PlaywrightExtraSharp.Plugins.Recaptcha.Provider; 4 | using PlaywrightExtraSharp.Utils; 5 | 6 | namespace PlaywrightExtraSharp.Plugins.Recaptcha; 7 | 8 | public class Recaptcha 9 | { 10 | private readonly CaptchaOptions _options; 11 | private readonly IRecaptchaProvider _provider; 12 | 13 | public Recaptcha(IRecaptchaProvider provider, CaptchaOptions options) 14 | { 15 | _provider = provider; 16 | _options = options; 17 | } 18 | 19 | public async Task Solve(IPage page) 20 | { 21 | try 22 | { 23 | var key = await GetKeyAsync(page); 24 | if (string.IsNullOrEmpty(key)) 25 | throw new InvalidOperationException(); 26 | var solution = await GetSolutionAsync(key!, page.Url); 27 | if (string.IsNullOrEmpty(solution)) 28 | throw new InvalidOperationException(); 29 | await WriteToInput(page, solution!); 30 | 31 | return new RecaptchaResult(); 32 | } 33 | catch (CaptchaException ex) 34 | { 35 | return new RecaptchaResult(false, ex); 36 | } 37 | } 38 | 39 | private async Task GetKeyAsync(IPage page) 40 | { 41 | var element = 42 | await page.QuerySelectorAsync("iframe[src^='https://www.google.com/recaptcha/api2/anchor'][name^=\"a-\"]"); 43 | 44 | if (element == null) 45 | throw new CaptchaException(page.Url, "Recaptcha key not found!"); 46 | 47 | var src = await element.GetPropertyAsync("src"); 48 | 49 | if (src == null) 50 | throw new CaptchaException(page.Url, "Recaptcha key not found!"); 51 | 52 | var key = HttpUtility.ParseQueryString(src.ToString()!).Get("k"); 53 | return key; 54 | } 55 | 56 | private async Task GetSolutionAsync(string key, string urlPage) 57 | { 58 | return await _provider.GetSolution(key, urlPage); 59 | } 60 | 61 | private async Task WriteToInput(IPage page, string value) 62 | { 63 | await page.EvaluateAsync( 64 | $"() => {{document.getElementById('g-recaptcha-response').innerHTML='{value}'}}"); 65 | 66 | 67 | var script = ResourceReader.ReadFile(GetType().Namespace + ".Scripts.EnterRecaptchaCallBackScript.js"); 68 | 69 | try 70 | { 71 | await page.EvaluateAsync($@"(value) => {{{script}}}", value); 72 | } 73 | catch 74 | { 75 | // ignored 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/RecaptchaResult.cs: -------------------------------------------------------------------------------- 1 | namespace PlaywrightExtraSharp.Plugins.Recaptcha; 2 | 3 | public class RecaptchaResult 4 | { 5 | public RecaptchaResult(bool isSuccess = true, CaptchaException? exception = null) 6 | { 7 | IsSuccess = isSuccess; 8 | Exception = exception; 9 | } 10 | 11 | public bool IsSuccess { get; } 12 | public CaptchaException? Exception { get; } 13 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/RestClient/PollingBuilder.cs: -------------------------------------------------------------------------------- 1 | using RestSharp; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.RestClient; 4 | 5 | public class PollingBuilder 6 | { 7 | private readonly RestSharp.RestClient _client; 8 | private readonly RestRequest _request; 9 | private int _limit = 5; 10 | private int _timeout = 5; 11 | 12 | public PollingBuilder(RestSharp.RestClient client, RestRequest request) 13 | { 14 | _client = client; 15 | _request = request; 16 | } 17 | 18 | public PollingBuilder WithTimeoutSeconds(int timeout) 19 | { 20 | _timeout = timeout; 21 | return this; 22 | } 23 | 24 | public PollingBuilder TriesLimit(int limit) 25 | { 26 | _limit = limit; 27 | return this; 28 | } 29 | 30 | public async Task> ActivatePollingAsync(Func, PollingAction> resultDelegate) 31 | { 32 | var response = await _client.ExecuteAsync(_request); 33 | 34 | if (resultDelegate(response) == PollingAction.Break || _limit <= 1) 35 | return response; 36 | 37 | await Task.Delay(_timeout * 1000); 38 | _limit -= 1; 39 | 40 | return await ActivatePollingAsync(resultDelegate); 41 | } 42 | } 43 | 44 | public enum PollingAction 45 | { 46 | ContinuePolling, 47 | Break 48 | } -------------------------------------------------------------------------------- /Plugins/Recaptcha/RestClient/RestClient.cs: -------------------------------------------------------------------------------- 1 | using RestSharp; 2 | 3 | namespace PlaywrightExtraSharp.Plugins.Recaptcha.RestClient; 4 | 5 | public class RestClient 6 | { 7 | private readonly RestSharp.RestClient _client; 8 | 9 | public RestClient(string? url = null) 10 | { 11 | _client = string.IsNullOrWhiteSpace(url) ? new RestSharp.RestClient() : new RestSharp.RestClient(url!); 12 | } 13 | 14 | public PollingBuilder CreatePollingBuilder(RestRequest request) 15 | { 16 | return new PollingBuilder(_client, request); 17 | } 18 | 19 | public async Task PostWithJsonAsync(string url, object content, CancellationToken token) 20 | { 21 | var request = new RestRequest(url); 22 | request.AddHeader("Content-type", "application/json"); 23 | request.AddJsonBody(content); 24 | request.Method = Method.Post; 25 | return await _client.PostAsync(request, token); 26 | } 27 | 28 | public async Task PostWithQueryAsync(string url, Dictionary query, 29 | CancellationToken token = default) 30 | { 31 | var request = new RestRequest(url) { Method = Method.Post }; 32 | 33 | foreach (var kvp in query) request.AddQueryParameter(kvp.Key, kvp.Value); 34 | 35 | return await _client.PostAsync(request, token); 36 | } 37 | 38 | private async Task> ExecuteAsync(RestRequest request, CancellationToken token) 39 | { 40 | return await _client.ExecuteAsync(request, token); 41 | } 42 | } -------------------------------------------------------------------------------- /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 | } else { 24 | c.callback = subpath + '.callback'; 25 | cb != c.callback ? c.function = cb : c.function = null; 26 | } 27 | } 28 | } 29 | } 30 | return c; 31 | } 32 | }); 33 | return (res)[0]; 34 | } else { 35 | return (null); 36 | } 37 | })() 38 | 39 | if (typeof (result.function) == 'function') { 40 | result.function(value) 41 | } else { 42 | eval(result.function).call(window, value); 43 | } 44 | -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/Scripts/HardwareConcurrency.js: -------------------------------------------------------------------------------- 1 | (concurrency) => { 2 | 3 | utils.replaceGetterWithProxy( 4 | Object.getPrototypeOf(navigator), 5 | 'hardwareConcurrency', 6 | utils.makeHandler().getterValue(concurrency) 7 | ) 8 | } -------------------------------------------------------------------------------- /Plugins/Scripts/Language.js: -------------------------------------------------------------------------------- 1 | (...languages) => { 2 | utils.replaceGetterWithProxy( 3 | Object.getPrototypeOf(navigator), 4 | 'languages', 5 | utils.makeHandler().getterValue(Object.freeze(languages)) 6 | ) 7 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/Scripts/Outdimensions.js: -------------------------------------------------------------------------------- 1 | () => { 2 | try { 3 | if (window.outerWidth && window.outerHeight) { 4 | return // nothing to do here 5 | } 6 | const windowFrame = 85 // probably OS and WM dependent 7 | window.outerWidth = window.innerWidth 8 | window.outerHeight = window.innerHeight + windowFrame 9 | } catch (err) { 10 | } 11 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/Scripts/Utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A set of shared utility functions specifically for the purpose of modifying native browser APIs without leaving traces. 3 | * 4 | * Meant to be passed down in puppeteer and used in the context of the page (everything in here runs in NodeJS as well as a browser). 5 | * 6 | * Note: If for whatever reason you need to use this outside of `puppeteer-extra`: 7 | * Just remove the `module.exports` statement at the very bottom, the rest can be copy pasted into any browser context. 8 | * 9 | * Alternatively take a look at the `extract-stealth-evasions` package to create a finished bundle which includes these utilities. 10 | * 11 | */ 12 | const utils = {} 13 | 14 | utils.init = () => { 15 | utils.preloadCache() 16 | } 17 | 18 | /** 19 | * Wraps a JS Proxy Handler and strips it's presence from error stacks, in case the traps throw. 20 | * 21 | * The presence of a JS Proxy can be revealed as it shows up in error stack traces. 22 | * 23 | * @param {object} handler - The JS Proxy handler to wrap 24 | */ 25 | utils.stripProxyFromErrors = (handler = {}) => { 26 | const newHandler = {} 27 | // We wrap each trap in the handler in a try/catch and modify the error stack if they throw 28 | const traps = Object.getOwnPropertyNames(handler) 29 | traps.forEach(trap => { 30 | newHandler[trap] = function () { 31 | try { 32 | // Forward the call to the defined proxy handler 33 | return handler[trap].apply(this, arguments || []) 34 | } catch (err) { 35 | // Stack traces differ per browser, we only support chromium based ones currently 36 | if (!err || !err.stack || !err.stack.includes(`at `)) { 37 | throw err 38 | } 39 | 40 | // When something throws within one of our traps the Proxy will show up in error stacks 41 | // An earlier implementation of this code would simply strip lines with a blacklist, 42 | // but it makes sense to be more surgical here and only remove lines related to our Proxy. 43 | // We try to use a known "anchor" line for that and strip it with everything above it. 44 | // If the anchor line cannot be found for some reason we fall back to our blacklist approach. 45 | 46 | const stripWithBlacklist = (stack, stripFirstLine = true) => { 47 | const blacklist = [ 48 | `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply 49 | `at Object.${trap} `, // e.g. Object.get or Object.apply 50 | `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-) 51 | ] 52 | return ( 53 | err.stack 54 | .split('\n') 55 | // Always remove the first (file) line in the stack (guaranteed to be our proxy) 56 | .filter((line, index) => !(index === 1 && stripFirstLine)) 57 | // Check if the line starts with one of our blacklisted strings 58 | .filter(line => !blacklist.some(bl => line.trim().startsWith(bl))) 59 | .join('\n') 60 | ) 61 | } 62 | 63 | const stripWithAnchor = (stack, anchor) => { 64 | const stackArr = stack.split('\n') 65 | anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium 66 | const anchorIndex = stackArr.findIndex(line => 67 | line.trim().startsWith(anchor) 68 | ) 69 | if (anchorIndex === -1) { 70 | return false // 404, anchor not found 71 | } 72 | // Strip everything from the top until we reach the anchor line 73 | // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`) 74 | stackArr.splice(1, anchorIndex) 75 | return stackArr.join('\n') 76 | } 77 | 78 | // Special cases due to our nested toString proxies 79 | err.stack = err.stack.replace( 80 | 'at Object.toString (', 81 | 'at Function.toString (' 82 | ) 83 | if ((err.stack || '').includes('at Function.toString (')) { 84 | err.stack = stripWithBlacklist(err.stack, false) 85 | throw err 86 | } 87 | 88 | // Try using the anchor method, fallback to blacklist if necessary 89 | err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack) 90 | 91 | throw err // Re-throw our now sanitized error 92 | } 93 | } 94 | }) 95 | return newHandler 96 | } 97 | 98 | /** 99 | * Strip error lines from stack traces until (and including) a known line the stack. 100 | * 101 | * @param {object} err - The error to sanitize 102 | * @param {string} anchor - The string the anchor line starts with 103 | */ 104 | utils.stripErrorWithAnchor = (err, anchor) => { 105 | const stackArr = err.stack.split('\n') 106 | const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor)) 107 | if (anchorIndex === -1) { 108 | return err // 404, anchor not found 109 | } 110 | // Strip everything from the top until we reach the anchor line (remove anchor line as well) 111 | // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`) 112 | stackArr.splice(1, anchorIndex) 113 | err.stack = stackArr.join('\n') 114 | return err 115 | } 116 | 117 | /** 118 | * Replace the property of an object in a stealthy way. 119 | * 120 | * Note: You also want to work on the prototype of an object most often, 121 | * as you'd otherwise leave traces (e.g. showing up in Object.getOwnPropertyNames(obj)). 122 | * 123 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty 124 | * 125 | * @example 126 | * replaceProperty(WebGLRenderingContext.prototype, 'getParameter', { value: "alice" }) 127 | * // or 128 | * replaceProperty(Object.getPrototypeOf(navigator), 'languages', { get: () => ['en-US', 'en'] }) 129 | * 130 | * @param {object} obj - The object which has the property to replace 131 | * @param {string} propName - The property name to replace 132 | * @param {object} descriptorOverrides - e.g. { value: "alice" } 133 | */ 134 | utils.replaceProperty = (obj, propName, descriptorOverrides = {}) => { 135 | return Object.defineProperty(obj, propName, { 136 | // Copy over the existing descriptors (writable, enumerable, configurable, etc) 137 | ...(Object.getOwnPropertyDescriptor(obj, propName) || {}), 138 | // Add our overrides (e.g. value, get()) 139 | ...descriptorOverrides 140 | }) 141 | } 142 | 143 | /** 144 | * Preload a cache of function copies and data. 145 | * 146 | * For a determined enough observer it would be possible to overwrite and sniff usage of functions 147 | * we use in our internal Proxies, to combat that we use a cached copy of those functions. 148 | * 149 | * Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before, 150 | * by executing `utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups). 151 | * 152 | * This is evaluated once per execution context (e.g. window) 153 | */ 154 | utils.preloadCache = () => { 155 | if (utils.cache) { 156 | return 157 | } 158 | utils.cache = { 159 | // Used in our proxies 160 | Reflect: { 161 | get: Reflect.get.bind(Reflect), 162 | apply: Reflect.apply.bind(Reflect) 163 | }, 164 | // Used in `makeNativeString` 165 | nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }` 166 | } 167 | } 168 | 169 | /** 170 | * Utility function to generate a cross-browser `toString` result representing native code. 171 | * 172 | * There's small differences: Chromium uses a single line, whereas FF & Webkit uses multiline strings. 173 | * To future-proof this we use an existing native toString result as the basis. 174 | * 175 | * The only advantage we have over the other team is that our JS runs first, hence we cache the result 176 | * of the native toString result once, so they cannot spoof it afterwards and reveal that we're using it. 177 | * 178 | * @example 179 | * makeNativeString('foobar') // => `function foobar() { [native code] }` 180 | * 181 | * @param {string} [name] - Optional function name 182 | */ 183 | utils.makeNativeString = (name = '') => { 184 | return utils.cache.nativeToStringStr.replace('toString', name || '') 185 | } 186 | 187 | /** 188 | * Helper function to modify the `toString()` result of the provided object. 189 | * 190 | * Note: Use `utils.redirectToString` instead when possible. 191 | * 192 | * There's a quirk in JS Proxies that will cause the `toString()` result to differ from the vanilla Object. 193 | * If no string is provided we will generate a `[native code]` thing based on the name of the property object. 194 | * 195 | * @example 196 | * patchToString(WebGLRenderingContext.prototype.getParameter, 'function getParameter() { [native code] }') 197 | * 198 | * @param {object} obj - The object for which to modify the `toString()` representation 199 | * @param {string} str - Optional string used as a return value 200 | */ 201 | utils.patchToString = (obj, str = '') => { 202 | const handler = { 203 | apply: function (target, ctx) { 204 | // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""` 205 | if (ctx === Function.prototype.toString) { 206 | return utils.makeNativeString('toString') 207 | } 208 | // `toString` targeted at our proxied Object detected 209 | if (ctx === obj) { 210 | // We either return the optional string verbatim or derive the most desired result automatically 211 | return str || utils.makeNativeString(obj.name) 212 | } 213 | // Check if the toString protype of the context is the same as the global prototype, 214 | // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case 215 | const hasSameProto = Object.getPrototypeOf( 216 | Function.prototype.toString 217 | ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins 218 | if (!hasSameProto) { 219 | // Pass the call on to the local Function.prototype.toString instead 220 | return ctx.toString() 221 | } 222 | return target.call(ctx) 223 | } 224 | } 225 | 226 | const toStringProxy = new Proxy( 227 | Function.prototype.toString, 228 | utils.stripProxyFromErrors(handler) 229 | ) 230 | utils.replaceProperty(Function.prototype, 'toString', { 231 | value: toStringProxy 232 | }) 233 | } 234 | 235 | /** 236 | * Make all nested functions of an object native. 237 | * 238 | * @param {object} obj 239 | */ 240 | utils.patchToStringNested = (obj = {}) => { 241 | return utils.execRecursively(obj, ['function'], utils.patchToString) 242 | } 243 | 244 | /** 245 | * Redirect toString requests from one object to another. 246 | * 247 | * @param {object} proxyObj - The object that toString will be called on 248 | * @param {object} originalObj - The object which toString result we wan to return 249 | */ 250 | utils.redirectToString = (proxyObj, originalObj) => { 251 | const handler = { 252 | apply: function (target, ctx) { 253 | // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""` 254 | if (ctx === Function.prototype.toString) { 255 | return utils.makeNativeString('toString') 256 | } 257 | 258 | // `toString` targeted at our proxied Object detected 259 | if (ctx === proxyObj) { 260 | const fallback = () => 261 | originalObj && originalObj.name 262 | ? utils.makeNativeString(originalObj.name) 263 | : utils.makeNativeString(proxyObj.name) 264 | 265 | // Return the toString representation of our original object if possible 266 | return originalObj + '' || fallback() 267 | } 268 | 269 | // Check if the toString protype of the context is the same as the global prototype, 270 | // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case 271 | const hasSameProto = Object.getPrototypeOf( 272 | Function.prototype.toString 273 | ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins 274 | if (!hasSameProto) { 275 | // Pass the call on to the local Function.prototype.toString instead 276 | return ctx.toString() 277 | } 278 | 279 | return target.call(ctx) 280 | } 281 | } 282 | 283 | const toStringProxy = new Proxy( 284 | Function.prototype.toString, 285 | utils.stripProxyFromErrors(handler) 286 | ) 287 | utils.replaceProperty(Function.prototype, 'toString', { 288 | value: toStringProxy 289 | }) 290 | } 291 | 292 | /** 293 | * All-in-one method to replace a property with a JS Proxy using the provided Proxy handler with traps. 294 | * 295 | * Will stealthify these aspects (strip error stack traces, redirect toString, etc). 296 | * Note: This is meant to modify native Browser APIs and works best with prototype objects. 297 | * 298 | * @example 299 | * replaceWithProxy(WebGLRenderingContext.prototype, 'getParameter', proxyHandler) 300 | * 301 | * @param {object} obj - The object which has the property to replace 302 | * @param {string} propName - The name of the property to replace 303 | * @param {object} handler - The JS Proxy handler to use 304 | */ 305 | utils.replaceWithProxy = (obj, propName, handler) => { 306 | const originalObj = obj[propName] 307 | const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler)) 308 | 309 | utils.replaceProperty(obj, propName, {value: proxyObj}) 310 | utils.redirectToString(proxyObj, originalObj) 311 | 312 | return true 313 | } 314 | /** 315 | * All-in-one method to replace a getter with a JS Proxy using the provided Proxy handler with traps. 316 | * 317 | * @example 318 | * replaceGetterWithProxy(Object.getPrototypeOf(navigator), 'vendor', proxyHandler) 319 | * 320 | * @param {object} obj - The object which has the property to replace 321 | * @param {string} propName - The name of the property to replace 322 | * @param {object} handler - The JS Proxy handler to use 323 | */ 324 | utils.replaceGetterWithProxy = (obj, propName, handler) => { 325 | const fn = Object.getOwnPropertyDescriptor(obj, propName).get 326 | const fnStr = fn.toString() // special getter function string 327 | const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler)) 328 | 329 | utils.replaceProperty(obj, propName, {get: proxyObj}) 330 | utils.patchToString(proxyObj, fnStr) 331 | 332 | return true 333 | } 334 | 335 | /** 336 | * All-in-one method to mock a non-existing property with a JS Proxy using the provided Proxy handler with traps. 337 | * 338 | * Will stealthify these aspects (strip error stack traces, redirect toString, etc). 339 | * 340 | * @example 341 | * mockWithProxy(chrome.runtime, 'sendMessage', function sendMessage() {}, proxyHandler) 342 | * 343 | * @param {object} obj - The object which has the property to replace 344 | * @param {string} propName - The name of the property to replace or create 345 | * @param {object} pseudoTarget - The JS Proxy target to use as a basis 346 | * @param {object} handler - The JS Proxy handler to use 347 | */ 348 | utils.mockWithProxy = (obj, propName, pseudoTarget, handler) => { 349 | const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler)) 350 | 351 | utils.replaceProperty(obj, propName, {value: proxyObj}) 352 | utils.patchToString(proxyObj) 353 | 354 | return true 355 | } 356 | 357 | /** 358 | * All-in-one method to create a new JS Proxy with stealth tweaks. 359 | * 360 | * This is meant to be used whenever we need a JS Proxy but don't want to replace or mock an existing known property. 361 | * 362 | * Will stealthify certain aspects of the Proxy (strip error stack traces, redirect toString, etc). 363 | * 364 | * @example 365 | * createProxy(navigator.mimeTypes.__proto__.namedItem, proxyHandler) // => Proxy 366 | * 367 | * @param {object} pseudoTarget - The JS Proxy target to use as a basis 368 | * @param {object} handler - The JS Proxy handler to use 369 | */ 370 | utils.createProxy = (pseudoTarget, handler) => { 371 | const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler)) 372 | utils.patchToString(proxyObj) 373 | 374 | return proxyObj 375 | } 376 | 377 | /** 378 | * Helper function to split a full path to an Object into the first part and property. 379 | * 380 | * @example 381 | * splitObjPath(`HTMLMediaElement.prototype.canPlayType`) 382 | * // => {objName: "HTMLMediaElement.prototype", propName: "canPlayType"} 383 | * 384 | * @param {string} objPath - The full path to an object as dot notation string 385 | */ 386 | utils.splitObjPath = objPath => ({ 387 | // Remove last dot entry (property) ==> `HTMLMediaElement.prototype` 388 | objName: objPath.split('.').slice(0, -1).join('.'), 389 | // Extract last dot entry ==> `canPlayType` 390 | propName: objPath.split('.').slice(-1)[0] 391 | }) 392 | 393 | /** 394 | * Convenience method to replace a property with a JS Proxy using the provided objPath. 395 | * 396 | * Supports a full path (dot notation) to the object as string here, in case that makes it easier. 397 | * 398 | * @example 399 | * replaceObjPathWithProxy('WebGLRenderingContext.prototype.getParameter', proxyHandler) 400 | * 401 | * @param {string} objPath - The full path to an object (dot notation string) to replace 402 | * @param {object} handler - The JS Proxy handler to use 403 | */ 404 | utils.replaceObjPathWithProxy = (objPath, handler) => { 405 | const {objName, propName} = utils.splitObjPath(objPath) 406 | const obj = eval(objName) // eslint-disable-line no-eval 407 | return utils.replaceWithProxy(obj, propName, handler) 408 | } 409 | 410 | /** 411 | * Traverse nested properties of an object recursively and apply the given function on a whitelist of value types. 412 | * 413 | * @param {object} obj 414 | * @param {array} typeFilter - e.g. `['function']` 415 | * @param {Function} fn - e.g. `utils.patchToString` 416 | */ 417 | utils.execRecursively = (obj = {}, typeFilter = [], fn) => { 418 | function recurse(obj) { 419 | for (const key in obj) { 420 | if (obj[key] === undefined) { 421 | continue 422 | } 423 | if (obj[key] && typeof obj[key] === 'object') { 424 | recurse(obj[key]) 425 | } else { 426 | if (obj[key] && typeFilter.includes(typeof obj[key])) { 427 | fn.call(this, obj[key]) 428 | } 429 | } 430 | } 431 | } 432 | 433 | recurse(obj) 434 | return obj 435 | } 436 | 437 | /** 438 | * Everything we run through e.g. `page.evaluate` runs in the browser context, not the NodeJS one. 439 | * That means we cannot just use reference variables and functions from outside code, we need to pass everything as a parameter. 440 | * 441 | * Unfortunately the data we can pass is only allowed to be of primitive types, regular functions don't survive the built-in serialization process. 442 | * This utility function will take an object with functions and stringify them, so we can pass them down unharmed as strings. 443 | * 444 | * We use this to pass down our utility functions as well as any other functions (to be able to split up code better). 445 | * 446 | * @see utils.materializeFns 447 | * 448 | * @param {object} fnObj - An object containing functions as properties 449 | */ 450 | utils.stringifyFns = (fnObj = {hello: () => 'world'}) => { 451 | // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine 452 | // https://github.com/feross/fromentries 453 | function fromEntries(iterable) { 454 | return [...iterable].reduce((obj, [key, val]) => { 455 | obj[key] = val 456 | return obj 457 | }, {}) 458 | } 459 | 460 | return (Object.fromEntries || fromEntries)( 461 | Object.entries(fnObj) 462 | .filter(([key, value]) => typeof value === 'function') 463 | .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval 464 | ) 465 | } 466 | 467 | /** 468 | * Utility function to reverse the process of `utils.stringifyFns`. 469 | * Will materialize an object with stringified functions (supports classic and fat arrow functions). 470 | * 471 | * @param {object} fnStrObj - An object containing stringified functions as properties 472 | */ 473 | utils.materializeFns = (fnStrObj = {hello: "() => 'world'"}) => { 474 | return Object.fromEntries( 475 | Object.entries(fnStrObj).map(([key, value]) => { 476 | if (value.startsWith('function')) { 477 | // some trickery is needed to make oldschool functions work :-) 478 | return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval 479 | } else { 480 | // arrow functions just work 481 | return [key, eval(value)] // eslint-disable-line no-eval 482 | } 483 | }) 484 | ) 485 | } 486 | 487 | // Proxy handler templates for re-usability 488 | utils.makeHandler = () => ({ 489 | // Used by simple `navigator` getter evasions 490 | getterValue: value => ({ 491 | apply(target, ctx, args) { 492 | // Let's fetch the value first, to trigger and escalate potential errors 493 | // Illegal invocations like `navigator.__proto__.vendor` will throw here 494 | const ret = utils.cache.Reflect.apply(...arguments) 495 | if (args && args.length === 0) { 496 | return value 497 | } 498 | return ret 499 | } 500 | }) 501 | }) 502 | 503 | utils.preloadCache(); -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /Plugins/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 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlaywrightExtraSharp 2 | 3 | [![NuGet Badge](https://buildstats.info/nuget/PlaywrightExtraSharp)](https://www.nuget.org/packages/PlaywrightExtraSharp) 4 | 5 | Based on [Puppeteer extra sharp](https://github.com/Overmiind/Puppeteer-sharp-extra) 6 | and [Node.js Playwright extra](https://github.com/berstend/puppeteer-extra/tree/master/packages/playwright-extra) 7 | 8 | ## Quickstart 9 | 10 | Long way 11 | 12 | ```csharp 13 | // Initialization plugin builder 14 | var playwrightExtra = new PlaywrightExtra(BrowserTypeEnum.Chromium); 15 | 16 | // Install browser 17 | playwrightExtra.Install(); 18 | 19 | // Use stealth plugin 20 | playwrightExtra.Use(new StealthExtraPlugin()); 21 | 22 | // Launch the puppeteer browser with plugins 23 | await playwrightExtra.LaunchAsync(new () 24 | { 25 | Headless = false 26 | }); 27 | 28 | // Create a new page 29 | var page = await playwrightExtra.NewPageAsync(); 30 | await page.GoToAsync("http://google.com"); 31 | ``` 32 | 33 | Compact way 34 | 35 | ```csharp 36 | // Initialize, install, use plugin and launch 37 | var playwrightExtra = await new PlaywrightExtra(BrowserTypeEnum.Chromium) 38 | .Install() 39 | .Use(new StealthExtraPlugin()) 40 | .LaunchAsync(new () 41 | { 42 | Headless = false 43 | }); 44 | 45 | // Create a new page 46 | var page = await playwrightExtra.NewPageAsync(); 47 | await page.GoToAsync("http://google.com"); 48 | ``` 49 | 50 | ## Note 51 | 52 | Because of how Playwright behaves with pages, you should only create new pages through PlaywrightExtra wrapper and not 53 | IBrowser. 54 | Please refer to examples above. 55 | 56 | ## Context Persistence 57 | 58 | There are 4 different ways to work with context in PlaywrightExtra: 59 | 60 | 1. launch incognito (as default was) with permanent context 61 | 62 | ```csharp 63 | .LaunchAsync(new () 64 | { 65 | Headless = false 66 | }, 67 | persistContext: true) 68 | ``` 69 | 70 | 2. launch incognito with new context per page (so when page is closed, context is disposed) 71 | 72 | ```csharp 73 | .LaunchAsync(new () 74 | { 75 | Headless = false 76 | }, 77 | persistContext: false) 78 | ``` 79 | 80 | 3. launch persistent (user data dir is considered) with permanent context 81 | 82 | ```csharp 83 | .LaunchPersistentAsync(new () 84 | { 85 | Headless = false 86 | }, 87 | persistContext: true) 88 | ``` 89 | 90 | 4. launch persistent with new context per page 91 | 92 | ```csharp 93 | .LaunchPersistentAsync(new () 94 | { 95 | Headless = false 96 | }, 97 | persistContext: false) 98 | ``` 99 | 100 | ## User data directory 101 | 102 | While running persistent mode you can specify path to user data directory. 103 | 104 | When context is persisted, specify directory at launch: 105 | 106 | ```csharp 107 | .LaunchPersistentAsync(new () 108 | { 109 | Headless = false 110 | }, 111 | persistContext: true, 112 | userDataDir: "/path/to/userdir") 113 | ``` 114 | 115 | When context is created for each page (```persistContext: false```), specify directory at page creation: 116 | 117 | ```csharp 118 | var page = await _playwrightExtra.NewPageAsync(userDataDir: "/path/to/userdir"); 119 | ``` 120 | 121 | ## Helpers 122 | 123 | For convenience, you can use helper method when performing request: 124 | 125 | ```csharp 126 | await page.GotoAndWaitForIdleAsync("http://google.com/", 127 | idleTime: TimeSpan.FromMilliseconds(1000)); 128 | ``` 129 | 130 | Basically this works like if you perform request with option ```WaitUntil = WaitUntilState.NetworkIdle``` 131 | 132 | But unlike builtin method, you can specify amount of time to wait after last request has fired. -------------------------------------------------------------------------------- /Utils/ResourceReader.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace PlaywrightExtraSharp.Utils; 4 | 5 | internal static class ResourceReader 6 | { 7 | public static string ReadFile(string path, Assembly? customAssembly = null) 8 | { 9 | var assembly = customAssembly ?? Assembly.GetExecutingAssembly(); 10 | using var stream = assembly.GetManifestResourceStream(path); 11 | if (stream is null) 12 | throw new FileNotFoundException($"File with path {path} not found!"); 13 | using var reader = new StreamReader(stream); 14 | return reader.ReadToEnd(); 15 | } 16 | } --------------------------------------------------------------------------------