Skip to content

Commit 099204e

Browse files
committed
Auto-login
1 parent f61b4d0 commit 099204e

5 files changed

Lines changed: 152 additions & 5 deletions

File tree

UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/IPuppeteerSettings.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public interface IPuppeteerSettings
2121
/// </summary>
2222
public string CaptchaCookieRetrievalAddress { get; }
2323
/// <summary>
24+
/// Address of API request for checking account existence used by Patreon login page. Finishing this request signifies being ready to enter password.
25+
/// </summary>
26+
public string AuthAddress { get; }
27+
/// <summary>
2428
/// Address of the remote browser, if not set internal browser will be used
2529
/// </summary>
2630
public Uri RemoteBrowserAddress { get; init; }
@@ -32,5 +36,13 @@ public interface IPuppeteerSettings
3236
/// Proxy server address
3337
/// </summary>
3438
public string ProxyServerAddress { get; init; }
39+
/// <summary>
40+
/// Email used for optional automatic login
41+
/// </summary>
42+
public string LoginEmail { get; init; }
43+
/// <summary>
44+
/// Password used for optional automatic login
45+
/// </summary>
46+
public string LoginPassword { get; init; }
3547
}
3648
}

UniversalDownloaderPlatform.PuppeteerEngine/Interfaces/Wrappers/Browser/IWebPage.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Threading.Tasks;
33
using PuppeteerSharp;
4+
using PuppeteerSharp.Input;
45

56
namespace UniversalDownloaderPlatform.PuppeteerEngine.Interfaces.Wrappers.Browser
67
{
@@ -15,6 +16,11 @@ public interface IWebPage
1516
Task SetUserAgentAsync(string userAgent);
1617
Task<string> GetContentAsync();
1718
Task<IWebRequest> WaitForRequestAsync(Func<IRequest, bool> predicate, WaitForOptions options = null);
19+
Task<IWebResponse> WaitForResponseAsync(Func<IResponse, bool> predicate, WaitForOptions options = null);
20+
Task WaitForNetworkIdleAsync(WaitForNetworkIdleOptions options = null);
21+
Task WaitForSelectorAsync(string selector, WaitForSelectorOptions options = null);
22+
Task TypeAsync(string selector, string text, TypeOptions options = null);
23+
Task ClickAsync(string selector, ClickOptions options = null);
1824
Task<CookieParam[]> GetCookiesAsync(params string[] urls);
1925
Task CloseAsync(PageCloseOptions options = null);
2026
}

UniversalDownloaderPlatform.PuppeteerEngine/PuppeteerCookieRetriever.cs

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using NLog;
88
using UniversalDownloaderPlatform.Common.Interfaces;
99
using PuppeteerSharp;
10+
using PuppeteerSharp.Input;
1011
using UniversalDownloaderPlatform.PuppeteerEngine.Interfaces;
1112
using UniversalDownloaderPlatform.PuppeteerEngine.Interfaces.Wrappers.Browser;
1213
using UniversalDownloaderPlatform.Common.Interfaces.Models;
@@ -23,7 +24,7 @@ public class PuppeteerCookieRetriever : ICookieRetriever, IDisposable
2324
private IPuppeteerSettings _settings;
2425
private bool _isHeadlessBrowser;
2526
private bool _isRemoteBrowser;
26-
27+
private bool _shouldTryAutoLogin;
2728

2829
/// <summary>
2930
/// Create new instance of PuppeteerCookieRetriever
@@ -40,6 +41,7 @@ public PuppeteerCookieRetriever()
4041
public Task BeforeStart(IUniversalDownloaderPlatformSettings settings)
4142
{
4243
_settings = settings as IPuppeteerSettings;
44+
_shouldTryAutoLogin = !string.IsNullOrWhiteSpace(_settings.LoginEmail) && !string.IsNullOrWhiteSpace(_settings.LoginPassword);
4345

4446
if (_settings.RemoteBrowserAddress != null)
4547
{
@@ -83,12 +85,12 @@ protected virtual async Task Login()
8385
if (!await IsLoggedIn(response))
8486
{
8587
_logger.Debug("We are NOT logged in, opening login page");
86-
if (_isRemoteBrowser)
88+
if (_isRemoteBrowser && !_shouldTryAutoLogin)
8789
{
8890
await page.CloseAsync();
8991
throw new Exception("You are not logged in into your account in remote browser. Please login and restart application.");
9092
}
91-
if (_puppeteerEngine.IsHeadless)
93+
if (_puppeteerEngine.IsHeadless && !_shouldTryAutoLogin)
9294
{
9395
_logger.Debug("Puppeteer is in headless mode, restarting in full mode");
9496
browser = await RestartBrowser(false);
@@ -100,7 +102,16 @@ protected virtual async Task Login()
100102
page.GoToAsync(_settings.LoginPageAddress, null);
101103
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
102104

103-
await page.WaitForRequestAsync(request => { return request.Url.Contains(_settings.LoginCheckAddress); });
105+
if (_shouldTryAutoLogin)
106+
{
107+
_logger.Debug("Credentials were supplied, attempting automatic Patreon login");
108+
await TryAutoLogin(page);
109+
}
110+
else
111+
{
112+
_logger.Debug("Waiting for user to log in Patreon manually");
113+
await page.WaitForRequestAsync(request => { return request.Url.Contains(_settings.LoginCheckAddress); });
114+
}
104115
}
105116
else
106117
{
@@ -118,6 +129,93 @@ protected virtual async Task Login()
118129
await page.CloseAsync();
119130
}
120131

132+
private async Task TryAutoLogin(IWebPage page)
133+
{
134+
try
135+
{
136+
await page.WaitForNetworkIdleAsync(new WaitForNetworkIdleOptions()
137+
{
138+
// The hanging requests are:
139+
// https://accounts.google.com/gsi/client
140+
// https://www.facebook.com/x/oauth/status
141+
// https://www.google.com/recaptcha/enterprise/webworker.js
142+
Concurrency = 3,
143+
IdleTime = 1000,
144+
Timeout = 10000,
145+
});
146+
}
147+
catch (WaitTaskTimeoutException) // In case there are other hanging requests (they seem to appear randomly)
148+
{
149+
_logger.Debug("Waiting for network idle timeout; proceed anyway and hope for the best");
150+
}
151+
152+
IWebResponse response = await EnterAndSubmit(page, "input[aria-label=\"Email\"]", _settings.LoginEmail);
153+
if ((await response.TextAsync()).Contains("\"next_auth_step\":\"signup\""))
154+
{
155+
throw new Exception("There does not exist an account with the provided email");
156+
}
157+
await EnterAndSubmit(page, "input[aria-label=\"Password\"]", _settings.LoginPassword);
158+
159+
// Not sure why this is needed, but otherwise GoToAsync will throw PuppeteerSharp.NavigationException: net::ERR_ABORTED
160+
await page.CloseAsync();
161+
}
162+
163+
private async Task<IWebResponse> EnterAndSubmit(IWebPage page, string selector, string text)
164+
{
165+
const string submitSelector = "button[type=\"submit\"][aria-disabled=\"false\"]";
166+
167+
await page.WaitForSelectorAsync(selector, new WaitForSelectorOptions() { Timeout = 10000 });
168+
_logger.Debug($"Found {selector}, entering information");
169+
await Task.Delay(300);
170+
171+
int retry;
172+
for (retry = 0; retry < 5; retry++)
173+
{
174+
await page.TypeAsync(selector, text, new TypeOptions() { Delay = 50 });
175+
try
176+
{
177+
await page.WaitForSelectorAsync(submitSelector, new WaitForSelectorOptions() { Timeout = 3000 });
178+
}
179+
catch (WaitTaskTimeoutException)
180+
{
181+
_logger.Debug($"Submit button did not appear; retrying {retry}/5");
182+
await page.ClickAsync(selector, new ClickOptions()
183+
{
184+
Count = 3, // hopefully select all text in the field
185+
Delay = 50,
186+
OffSet = new Offset(10, 10)
187+
});
188+
continue;
189+
}
190+
break;
191+
}
192+
if (retry == 5)
193+
{
194+
throw new Exception("Cannot find the submit button after 5 tries");
195+
}
196+
197+
await Task.Delay(300);
198+
await page.ClickAsync(submitSelector);
199+
200+
IWebResponse authResponse = await page.WaitForResponseAsync(
201+
response => { return response.Url.Contains(_settings.AuthAddress); },
202+
new WaitForOptions() { Timeout = 10000 }
203+
);
204+
switch (authResponse.Status)
205+
{
206+
case HttpStatusCode.OK:
207+
return authResponse;
208+
case HttpStatusCode.BadRequest:
209+
throw new Exception($"Auth returned non-OK code: {authResponse.Status}; you probably provided an invalid email");
210+
case HttpStatusCode.TooManyRequests:
211+
throw new Exception($"Auth returned non-OK code: {authResponse.Status}; you probably tried logging in for too many times");
212+
case HttpStatusCode.Forbidden:
213+
throw new Exception($"Auth returned non-OK code: {authResponse.Status}; you either provided a wrong password or are blocked");
214+
default:
215+
throw new Exception($"Auth returned non-OK code: {authResponse.Status}");
216+
}
217+
}
218+
121219
/// <summary>
122220
/// Perform check if the received response contains data which can be used to assume that we are logged in
123221
/// </summary>

UniversalDownloaderPlatform.PuppeteerEngine/UniversalDownloaderPlatform.PuppeteerEngine.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<PackageReference Include="Ninject" Version="3.3.6" />
1010
<PackageReference Include="ninject.extensions.conventions" Version="3.3.0" />
1111
<PackageReference Include="NLog" Version="6.0.3" />
12-
<PackageReference Include="PuppeteerSharp" Version="20.2.2" />
12+
<PackageReference Include="PuppeteerSharp" Version="24.40.0" />
1313
</ItemGroup>
1414

1515
<ItemGroup>

UniversalDownloaderPlatform.PuppeteerEngine/Wrappers/Browser/WebPage.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Threading.Tasks;
44
using PuppeteerSharp;
5+
using PuppeteerSharp.Input;
56
using UniversalDownloaderPlatform.PuppeteerEngine.Interfaces.Wrappers.Browser;
67

78
namespace UniversalDownloaderPlatform.PuppeteerEngine.Wrappers.Browser
@@ -55,6 +56,36 @@ public async Task<IWebRequest> WaitForRequestAsync(Func<IRequest, bool> predicat
5556
return webRequest;
5657
}
5758

59+
public async Task<IWebResponse> WaitForResponseAsync(Func<IResponse, bool> predicate, WaitForOptions options = null)
60+
{
61+
await ConfigurePage();
62+
IResponse response = await _page.WaitForResponseAsync(predicate, options);
63+
IWebResponse webResponse = new WebResponse(response);
64+
return webResponse;
65+
}
66+
67+
public async Task WaitForNetworkIdleAsync(WaitForNetworkIdleOptions options = null)
68+
{
69+
await ConfigurePage();
70+
await _page.WaitForNetworkIdleAsync(options);
71+
}
72+
73+
public async Task WaitForSelectorAsync(string selector, WaitForSelectorOptions options = null)
74+
{
75+
await ConfigurePage();
76+
await _page.WaitForSelectorAsync(selector, options);
77+
}
78+
79+
public async Task TypeAsync(string selector, string text, TypeOptions options = null)
80+
{
81+
await _page.TypeAsync(selector, text, options);
82+
}
83+
84+
public async Task ClickAsync(string selector, ClickOptions options = null)
85+
{
86+
await _page.ClickAsync(selector, options);
87+
}
88+
5889
public async Task<CookieParam[]> GetCookiesAsync(params string[] urls)
5990
{
6091
return await _page.GetCookiesAsync(urls);

0 commit comments

Comments
 (0)