Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public interface IPuppeteerSettings
/// </summary>
public string CaptchaCookieRetrievalAddress { get; }
/// <summary>
/// Address of API request for checking account existence used by Patreon login page. Finishing this request signifies being ready to enter password.
/// </summary>
public string AuthAddress { get; }
/// <summary>
/// Address of the remote browser, if not set internal browser will be used
/// </summary>
public Uri RemoteBrowserAddress { get; init; }
Expand All @@ -32,5 +36,13 @@ public interface IPuppeteerSettings
/// Proxy server address
/// </summary>
public string ProxyServerAddress { get; init; }
/// <summary>
/// Email used for optional automatic login
/// </summary>
public string LoginEmail { get; init; }
/// <summary>
/// Password used for optional automatic login
/// </summary>
public string LoginPassword { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using PuppeteerSharp;
using PuppeteerSharp.Input;

namespace UniversalDownloaderPlatform.PuppeteerEngine.Interfaces.Wrappers.Browser
{
Expand All @@ -15,6 +16,11 @@ public interface IWebPage
Task SetUserAgentAsync(string userAgent);
Task<string> GetContentAsync();
Task<IWebRequest> WaitForRequestAsync(Func<IRequest, bool> predicate, WaitForOptions options = null);
Task<IWebResponse> WaitForResponseAsync(Func<IResponse, bool> 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<CookieParam[]> GetCookiesAsync(params string[] urls);
Task CloseAsync(PageCloseOptions options = null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,7 +24,7 @@ public class PuppeteerCookieRetriever : ICookieRetriever, IDisposable
private IPuppeteerSettings _settings;
private bool _isHeadlessBrowser;
private bool _isRemoteBrowser;

private bool _shouldTryAutoLogin;

/// <summary>
/// Create new instance of PuppeteerCookieRetriever
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
Expand All @@ -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
{
Expand All @@ -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<IWebResponse> 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}");
}
}

/// <summary>
/// Perform check if the received response contains data which can be used to assume that we are logged in
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageReference Include="Ninject" Version="3.3.6" />
<PackageReference Include="ninject.extensions.conventions" Version="3.3.0" />
<PackageReference Include="NLog" Version="6.0.3" />
<PackageReference Include="PuppeteerSharp" Version="20.2.2" />
<PackageReference Include="PuppeteerSharp" Version="24.40.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,6 +56,36 @@ public async Task<IWebRequest> WaitForRequestAsync(Func<IRequest, bool> predicat
return webRequest;
}

public async Task<IWebResponse> WaitForResponseAsync(Func<IResponse, bool> 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<CookieParam[]> GetCookiesAsync(params string[] urls)
{
return await _page.GetCookiesAsync(urls);
Expand Down