diff --git a/UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/IPuppeteerSettings.cs b/UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/IPuppeteerSettings.cs index 410e20c..a1c1295 100644 --- a/UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/IPuppeteerSettings.cs +++ b/UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/IPuppeteerSettings.cs @@ -21,6 +21,10 @@ public interface IPuppeteerSettings /// public string CaptchaCookieRetrievalAddress { get; } /// + /// Address of API request for checking account existence used by Patreon login page. Finishing this request signifies being ready to enter password. + /// + public string AuthAddress { get; } + /// /// Address of the remote browser, if not set internal browser will be used /// public Uri RemoteBrowserAddress { get; init; } @@ -32,5 +36,13 @@ public interface IPuppeteerSettings /// Proxy server address /// public string ProxyServerAddress { get; init; } + /// + /// Email used for optional automatic login + /// + public string LoginEmail { get; init; } + /// + /// Password used for optional automatic login + /// + public string LoginPassword { get; init; } } } diff --git a/UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/Wrappers/Browser/IWebPage.cs b/UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/Wrappers/Browser/IWebPage.cs index bc93511..d725637 100644 --- a/UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/Wrappers/Browser/IWebPage.cs +++ b/UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/Wrappers/Browser/IWebPage.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using PuppeteerSharp; +using PuppeteerSharp.Input; namespace UniversalDownloaderPlatform.PuppeteerEngine.Interfaces.Wrappers.Browser { @@ -15,6 +16,11 @@ public interface IWebPage Task SetUserAgentAsync(string userAgent); Task GetContentAsync(); Task WaitForRequestAsync(Func predicate, WaitForOptions options = null); + Task WaitForResponseAsync(Func predicate, WaitForOptions options = null); + Task WaitForNetworkIdleAsync(WaitForNetworkIdleOptions options = null); + Task WaitForSelectorAsync(string selector, WaitForSelectorOptions options = null); + Task TypeAsync(string selector, string text, TypeOptions options = null); + Task ClickAsync(string selector, ClickOptions options = null); Task GetCookiesAsync(params string[] urls); Task CloseAsync(PageCloseOptions options = null); } diff --git a/UniversalDownloaderPlatform.PuppeteerEngine/PuppeteerCookieRetriever.cs b/UniversalDownloaderPlatform.PuppeteerEngine/PuppeteerCookieRetriever.cs index 49cca23..8331ca0 100644 --- a/UniversalDownloaderPlatform.PuppeteerEngine/PuppeteerCookieRetriever.cs +++ b/UniversalDownloaderPlatform.PuppeteerEngine/PuppeteerCookieRetriever.cs @@ -7,6 +7,7 @@ using NLog; using UniversalDownloaderPlatform.Common.Interfaces; using PuppeteerSharp; +using PuppeteerSharp.Input; using UniversalDownloaderPlatform.PuppeteerEngine.Interfaces; using UniversalDownloaderPlatform.PuppeteerEngine.Interfaces.Wrappers.Browser; using UniversalDownloaderPlatform.Common.Interfaces.Models; @@ -23,7 +24,7 @@ public class PuppeteerCookieRetriever : ICookieRetriever, IDisposable private IPuppeteerSettings _settings; private bool _isHeadlessBrowser; private bool _isRemoteBrowser; - + private bool _shouldTryAutoLogin; /// /// Create new instance of PuppeteerCookieRetriever @@ -40,6 +41,7 @@ public PuppeteerCookieRetriever() public Task BeforeStart(IUniversalDownloaderPlatformSettings settings) { _settings = settings as IPuppeteerSettings; + _shouldTryAutoLogin = !string.IsNullOrWhiteSpace(_settings.LoginEmail) && !string.IsNullOrWhiteSpace(_settings.LoginPassword); if (_settings.RemoteBrowserAddress != null) { @@ -83,12 +85,12 @@ protected virtual async Task Login() if (!await IsLoggedIn(response)) { _logger.Debug("We are NOT logged in, opening login page"); - if (_isRemoteBrowser) + if (_isRemoteBrowser && !_shouldTryAutoLogin) { await page.CloseAsync(); throw new Exception("You are not logged in into your account in remote browser. Please login and restart application."); } - if (_puppeteerEngine.IsHeadless) + if (_puppeteerEngine.IsHeadless && !_shouldTryAutoLogin) { _logger.Debug("Puppeteer is in headless mode, restarting in full mode"); browser = await RestartBrowser(false); @@ -100,7 +102,16 @@ protected virtual async Task Login() page.GoToAsync(_settings.LoginPageAddress, null); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - await page.WaitForRequestAsync(request => { return request.Url.Contains(_settings.LoginCheckAddress); }); + if (_shouldTryAutoLogin) + { + _logger.Debug("Credentials were supplied, attempting automatic Patreon login"); + await TryAutoLogin(page); + } + else + { + _logger.Debug("Waiting for user to log in Patreon manually"); + await page.WaitForRequestAsync(request => { return request.Url.Contains(_settings.LoginCheckAddress); }); + } } else { @@ -118,6 +129,93 @@ protected virtual async Task Login() await page.CloseAsync(); } + private async Task TryAutoLogin(IWebPage page) + { + try + { + await page.WaitForNetworkIdleAsync(new WaitForNetworkIdleOptions() + { + // The hanging requests are: + // https://accounts.google.com/gsi/client + // https://www.facebook.com/x/oauth/status + // https://www.google.com/recaptcha/enterprise/webworker.js + Concurrency = 3, + IdleTime = 1000, + Timeout = 10000, + }); + } + catch (Exception ex) when (ex is WaitTaskTimeoutException || ex is TimeoutException) // In case there are other hanging requests (they seem to appear randomly) + { + _logger.Debug("Waiting for network idle timeout; proceed anyway and hope for the best"); + } + + IWebResponse response = await EnterAndSubmit(page, "input[aria-label=\"Email\"]", _settings.LoginEmail); + if ((await response.TextAsync()).Contains("\"next_auth_step\":\"signup\"")) + { + throw new Exception("There does not exist an account with the provided email"); + } + await EnterAndSubmit(page, "input[aria-label=\"Password\"]", _settings.LoginPassword); + + // Not sure why this is needed, but otherwise GoToAsync will throw PuppeteerSharp.NavigationException: net::ERR_ABORTED + await page.CloseAsync(); + } + + private async Task EnterAndSubmit(IWebPage page, string selector, string text) + { + const string submitSelector = "button[type=\"submit\"][aria-disabled=\"false\"]"; + + await page.WaitForSelectorAsync(selector, new WaitForSelectorOptions() { Timeout = 10000 }); + _logger.Debug($"Found {selector}, entering information"); + await Task.Delay(300); + + int retry; + for (retry = 0; retry < 5; retry++) + { + await page.TypeAsync(selector, text, new TypeOptions() { Delay = 50 }); + try + { + await page.WaitForSelectorAsync(submitSelector, new WaitForSelectorOptions() { Timeout = 3000 }); + } + catch (Exception ex) when (ex is WaitTaskTimeoutException || ex is TimeoutException) + { + _logger.Debug($"Submit button did not appear; retrying {retry}/5"); + await page.ClickAsync(selector, new ClickOptions() + { + Count = 3, // hopefully select all text in the field + Delay = 50, + OffSet = new Offset(10, 10) + }); + continue; + } + break; + } + if (retry == 5) + { + throw new Exception("Cannot find the submit button after 5 tries"); + } + + await Task.Delay(300); + await page.ClickAsync(submitSelector); + + IWebResponse authResponse = await page.WaitForResponseAsync( + response => { return response.Url.Contains(_settings.AuthAddress); }, + new WaitForOptions() { Timeout = 10000 } + ); + switch (authResponse.Status) + { + case HttpStatusCode.OK: + return authResponse; + case HttpStatusCode.BadRequest: + throw new Exception($"Auth returned non-OK code: {authResponse.Status}; you probably provided an invalid email"); + case HttpStatusCode.TooManyRequests: + throw new Exception($"Auth returned non-OK code: {authResponse.Status}; you probably tried logging in for too many times"); + case HttpStatusCode.Forbidden: + throw new Exception($"Auth returned non-OK code: {authResponse.Status}; you either provided a wrong password or are blocked"); + default: + throw new Exception($"Auth returned non-OK code: {authResponse.Status}"); + } + } + /// /// Perform check if the received response contains data which can be used to assume that we are logged in /// diff --git a/UniversalDownloaderPlatform.PuppeteerEngine/UniversalDownloaderPlatform.PuppeteerEngine.csproj b/UniversalDownloaderPlatform.PuppeteerEngine/UniversalDownloaderPlatform.PuppeteerEngine.csproj index a2a0c00..cca715a 100644 --- a/UniversalDownloaderPlatform.PuppeteerEngine/UniversalDownloaderPlatform.PuppeteerEngine.csproj +++ b/UniversalDownloaderPlatform.PuppeteerEngine/UniversalDownloaderPlatform.PuppeteerEngine.csproj @@ -9,7 +9,7 @@ - + diff --git a/UniversalDownloaderPlatform.PuppeteerEngine/Wrappers/Browser/WebPage.cs b/UniversalDownloaderPlatform.PuppeteerEngine/Wrappers/Browser/WebPage.cs index 09138ce..71f1487 100644 --- a/UniversalDownloaderPlatform.PuppeteerEngine/Wrappers/Browser/WebPage.cs +++ b/UniversalDownloaderPlatform.PuppeteerEngine/Wrappers/Browser/WebPage.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using PuppeteerSharp; +using PuppeteerSharp.Input; using UniversalDownloaderPlatform.PuppeteerEngine.Interfaces.Wrappers.Browser; namespace UniversalDownloaderPlatform.PuppeteerEngine.Wrappers.Browser @@ -55,6 +56,36 @@ public async Task WaitForRequestAsync(Func predicat return webRequest; } + public async Task WaitForResponseAsync(Func predicate, WaitForOptions options = null) + { + await ConfigurePage(); + IResponse response = await _page.WaitForResponseAsync(predicate, options); + IWebResponse webResponse = new WebResponse(response); + return webResponse; + } + + public async Task WaitForNetworkIdleAsync(WaitForNetworkIdleOptions options = null) + { + await ConfigurePage(); + await _page.WaitForNetworkIdleAsync(options); + } + + public async Task WaitForSelectorAsync(string selector, WaitForSelectorOptions options = null) + { + await ConfigurePage(); + await _page.WaitForSelectorAsync(selector, options); + } + + public async Task TypeAsync(string selector, string text, TypeOptions options = null) + { + await _page.TypeAsync(selector, text, options); + } + + public async Task ClickAsync(string selector, ClickOptions options = null) + { + await _page.ClickAsync(selector, options); + } + public async Task GetCookiesAsync(params string[] urls) { return await _page.GetCookiesAsync(urls);