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);