Removed delays from Puppeteer Login and IsLoggedIn tasks. Finished those tasks. Added browser operation timeout configuration.
This commit is contained in:
parent
06b461985c
commit
8231cedcf1
@ -30,6 +30,7 @@
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.8" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.13" />
|
||||
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
||||
<PackageReference Include="PuppeteerSharp" Version="15.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.35.0" />
|
||||
|
@ -5,6 +5,8 @@ public class PuppeteerConfig
|
||||
public int KeepAliveIntervalMinutes { get; set; }
|
||||
public int CheckForNewDataIntervalMinutes { get; set; }
|
||||
public int TaskCheckIntervalMinutes { get; set; }
|
||||
public string SimmonsBankBaseUrl { get; set; }
|
||||
public int BrowserOperationTimeoutSeconds { get; set; }
|
||||
}
|
||||
|
||||
public class PuppeteerConfigConstants
|
||||
@ -12,4 +14,6 @@ public class PuppeteerConfigConstants
|
||||
public const string KeepAliveIntervalMinutes = "KeepAliveIntervalMinutes";
|
||||
public const string CheckForNewDataIntervalMinutes = "CheckForNewDataIntervalMinutes";
|
||||
public const string TaskCheckIntervalMinutes = "TaskCheckIntervalMinutes";
|
||||
public const string SimmonsBankBaseUrl = "SimmonsBankBaseUrl";
|
||||
public const string BrowserOperationTimeoutSeconds = "BrowserOperationTimeoutSeconds";
|
||||
}
|
@ -6,5 +6,6 @@ namespace AAIntegration.SimmonsBank.API.Processes;
|
||||
|
||||
public interface IPuppeteerProcess
|
||||
{
|
||||
void SetStoppingToken(CancellationToken stoppingToken);
|
||||
Task StayLoggedIn(User user);
|
||||
}
|
||||
|
@ -8,31 +8,215 @@ using AAIntegration.SimmonsBank.API.Services;
|
||||
using Internal;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PuppeteerSharp;
|
||||
using OtpNet;
|
||||
using System.Text;
|
||||
|
||||
namespace AAIntegration.SimmonsBank.API.Processes;
|
||||
|
||||
public class PuppeteerProcess : IPuppeteerProcess
|
||||
{
|
||||
private readonly PuppeteerConfig _config;
|
||||
private readonly ILogger<PuppeteerProcess> _logger;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly PuppeteerConfig _config;
|
||||
private readonly ILogger<PuppeteerProcess> _logger;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private const string BROWSER_CACHE_KEY = "BrowserCacheKey";
|
||||
private const string DASHBOARD_SELECTOR = "body > banno-web > bannoweb-layout > bannoweb-dashboard";
|
||||
private CancellationToken _stoppingToken;
|
||||
|
||||
public PuppeteerProcess(
|
||||
public PuppeteerProcess(
|
||||
IOptions<PuppeteerConfig> config,
|
||||
ILogger<PuppeteerProcess> logger,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
{
|
||||
_config = config.Value;
|
||||
_logger = logger;
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
_stoppingToken = new CancellationToken();
|
||||
}
|
||||
|
||||
public void SetStoppingToken(CancellationToken stoppingToken)
|
||||
{
|
||||
_stoppingToken = stoppingToken;
|
||||
}
|
||||
|
||||
public async Task StayLoggedIn(User user)
|
||||
{
|
||||
_logger.LogInformation($"... doing work and processing for user {user.Id} ...");
|
||||
|
||||
IBrowser browser = await GetUserBrowserAsync(user);
|
||||
|
||||
if (!await IsLoggedIn(user, browser))
|
||||
{
|
||||
await Login(user, browser);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper Functions
|
||||
|
||||
|
||||
private async Task Delay(int milliseconds)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(milliseconds), _stoppingToken);
|
||||
}
|
||||
|
||||
private async Task<IBrowser> GetUserBrowserAsync(User user)
|
||||
{
|
||||
if (_memoryCache.TryGetValue<Dictionary<int, IBrowser>>(BROWSER_CACHE_KEY, out var internalKeys))
|
||||
{
|
||||
List<KeyValuePair<int, IBrowser>> list = internalKeys.Where(x => x.Key == user.Id).ToList();
|
||||
|
||||
if (list.Count > 0 && list.First().Value != null)
|
||||
{
|
||||
_logger.LogInformation($"Found the browser for user with id '{user.Id}'.");
|
||||
return list.First().Value;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Could NOT find the browser for user with id '{user.Id}'. About to create one...");
|
||||
|
||||
using var browserFetcher = new BrowserFetcher();
|
||||
await browserFetcher.DownloadAsync();
|
||||
|
||||
var options = new LaunchOptions {
|
||||
Headless = false,
|
||||
IgnoreHTTPSErrors = true
|
||||
};
|
||||
|
||||
IBrowser browser = await Puppeteer.LaunchAsync(options);
|
||||
|
||||
internalKeys ??= new Dictionary<int, IBrowser>();
|
||||
internalKeys.Add(user.Id, browser);
|
||||
_memoryCache.Set(BROWSER_CACHE_KEY, internalKeys);
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
private async Task<bool> Login(User user, IBrowser browser)
|
||||
{
|
||||
TimeSpan timeout = TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds);
|
||||
|
||||
// Setup Page
|
||||
await using IPage page = await browser.NewPageAsync();
|
||||
await page.SetViewportAsync(new ViewPortOptions { Width = 1200, Height = 720 });
|
||||
WaitUntilNavigation[] waitUntils = { WaitUntilNavigation.Networkidle0 };
|
||||
|
||||
// Navigate to login screen
|
||||
await page.GoToAsync(_config.SimmonsBankBaseUrl + "/login");//, null, waitUntils); // wait until page load
|
||||
|
||||
try
|
||||
{
|
||||
// Type username
|
||||
string selector = "#username";
|
||||
await page.WaitForSelectorAsync(selector).WaitAsync(timeout, _stoppingToken);
|
||||
await page.TypeAsync(selector, user.SimmonsBankUsername);
|
||||
|
||||
// Press 1st Submit Button
|
||||
selector = "jha-button";
|
||||
await page.WaitForSelectorAsync(selector).WaitAsync(timeout, _stoppingToken);
|
||||
await page.ClickAsync(selector);
|
||||
|
||||
// Type password
|
||||
selector = "#password";
|
||||
await page.WaitForSelectorAsync(selector).WaitAsync(timeout, _stoppingToken);
|
||||
await page.TypeAsync(selector, user.SimmonsBankPassword);
|
||||
|
||||
// Click SignIn button - In Chrome -> JS Path worked well
|
||||
selector = "#login-password-form > bannoweb-flex-wrapper:nth-child(5) > div > jha-button";
|
||||
await page.WaitForSelectorAsync(selector).WaitAsync(timeout, _stoppingToken);
|
||||
IElementHandle signInButton = await page.QuerySelectorAsync(selector);
|
||||
|
||||
if (signInButton != null)
|
||||
{
|
||||
await signInButton.ClickAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Failed to find Sign-In button");
|
||||
return false;
|
||||
}
|
||||
|
||||
await page.WaitForNetworkIdleAsync();
|
||||
|
||||
// Find TOTP input
|
||||
selector = "body > banno-web > bannoweb-login > bannoweb-login-steps > bannoweb-two-factor-verify > jha-slider > jha-slider-content > jha-slider-pane:nth-child(4) > bannoweb-two-factor-enter-code > article > form > jha-form-floating-group > input[type=text]";
|
||||
await page.WaitForSelectorAsync(selector).WaitAsync(timeout, _stoppingToken);
|
||||
//await Delay(150);
|
||||
|
||||
// Generate TOTP code
|
||||
Totp totpInstance = new Totp(Base32Encoding.ToBytes(user.MFAKey));
|
||||
string totpCode = totpInstance.ComputeTotp();
|
||||
|
||||
// Type TOTP code
|
||||
IElementHandle totpInput = await page.QuerySelectorAsync(selector);
|
||||
await totpInput.TypeAsync(totpCode);
|
||||
|
||||
// Click Verify Button
|
||||
selector = "body > banno-web > bannoweb-login > bannoweb-login-steps > bannoweb-two-factor-verify > jha-slider > jha-slider-content > jha-slider-pane:nth-child(4) > bannoweb-two-factor-enter-code > article > form > jha-button";
|
||||
await page.WaitForSelectorAsync(selector).WaitAsync(timeout, _stoppingToken);
|
||||
IElementHandle verifyButton = await page.QuerySelectorAsync(selector);
|
||||
if (verifyButton != null)
|
||||
{
|
||||
await verifyButton.ClickAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Failed to find Verify button");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await page.WaitForSelectorAsync(DASHBOARD_SELECTOR).WaitAsync(timeout, _stoppingToken);
|
||||
}
|
||||
catch(TimeoutException)
|
||||
{
|
||||
_logger.LogWarning($"Dashboard isn't loading after login for user '{user.Id}'");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Dashboard found for '{user.Id}'");
|
||||
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
_logger.LogError($"Login Task for user '{user.Id}' was canceled");
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
_logger.LogWarning($"Login Task timed out for user '{user.Id}' after {timeout} seconds");
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await page.CloseAsync();
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Login completed for user {user.Id}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> IsLoggedIn(User user, IBrowser browser)
|
||||
{
|
||||
// Setup Page
|
||||
await using IPage page = await browser.NewPageAsync();
|
||||
await page.SetViewportAsync(new ViewPortOptions { Width = 1200, Height = 720 });
|
||||
|
||||
// Navigate to home screen
|
||||
await page.GoToAsync(_config.SimmonsBankBaseUrl);
|
||||
|
||||
try
|
||||
{
|
||||
await page.WaitForSelectorAsync(DASHBOARD_SELECTOR).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds), _stoppingToken);
|
||||
}
|
||||
catch(TaskCanceledException)
|
||||
{
|
||||
_logger.LogWarning($"IsLoggedIn Task for user '{user.Id}' was canceled");
|
||||
}
|
||||
catch(TimeoutException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ public class PuppeteerWorker : BackgroundService
|
||||
using (var scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
_userService = scope.ServiceProvider.GetService<IUserService>();
|
||||
_puppeteerProcess.SetStoppingToken(stoppingToken);
|
||||
|
||||
// This is how we keep the app running (in the background)
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
@ -74,7 +75,7 @@ public class PuppeteerWorker : BackgroundService
|
||||
|
||||
//_logger.LogInformation($"KeepAlive configured to {ka}. This user hasn't been kept alive in {uka} minute(s).");
|
||||
|
||||
if (_config.KeepAliveIntervalMinutes < (int)DateTime.UtcNow.Subtract(GetUserLastExecution(PuppeteerConfigConstants.KeepAliveIntervalMinutes, user.Id)).TotalMinutes)
|
||||
if (_config.KeepAliveIntervalMinutes <= (int)DateTime.UtcNow.Subtract(GetUserLastExecution(PuppeteerConfigConstants.KeepAliveIntervalMinutes, user.Id)).TotalMinutes)
|
||||
{
|
||||
await _puppeteerProcess.StayLoggedIn(user);
|
||||
SetUserLastExecution(PuppeteerConfigConstants.KeepAliveIntervalMinutes, user.Id, DateTime.UtcNow);
|
||||
|
@ -26,8 +26,10 @@
|
||||
"APIUrl": "https://localhost:7260"
|
||||
},
|
||||
"Puppeteer": {
|
||||
"BrowserOperationTimeoutSeconds": 5,
|
||||
"TaskCheckIntervalMinutes": 1,
|
||||
"KeepAliveIntervalMinutes": 10,
|
||||
"CheckForNewDataIntervalMinutes": 15
|
||||
"KeepAliveIntervalMinutes": 1,
|
||||
"CheckForNewDataIntervalMinutes": 15,
|
||||
"SimmonsBankBaseUrl": "https://login.simmonsbank.com"
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ This is an integration for ActiveAllocator.
|
||||
|
||||
The type is Transaction Importer, specifically created for interfacing with SimmonsBank's online banking website.
|
||||
|
||||
## Dependencies
|
||||
|
||||
[Otp.NET library](https://github.com/kspearrin/Otp.NET)
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
On Archlinux install the following to use dotnet: ```sudo pacman -Sy dotnet-sdk dotnet-runtime aspnet-runtime```.
|
||||
|
Loading…
x
Reference in New Issue
Block a user