Removed delays from Puppeteer Login and IsLoggedIn tasks. Finished those tasks. Added browser operation timeout configuration.

This commit is contained in:
William Lewis 2024-04-02 21:52:01 -05:00
parent 06b461985c
commit 8231cedcf1
7 changed files with 207 additions and 10 deletions

View File

@ -30,6 +30,7 @@
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.8" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.8" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.13" /> <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="PuppeteerSharp" Version="15.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.35.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.35.0" />

View File

@ -5,6 +5,8 @@ public class PuppeteerConfig
public int KeepAliveIntervalMinutes { get; set; } public int KeepAliveIntervalMinutes { get; set; }
public int CheckForNewDataIntervalMinutes { get; set; } public int CheckForNewDataIntervalMinutes { get; set; }
public int TaskCheckIntervalMinutes { get; set; } public int TaskCheckIntervalMinutes { get; set; }
public string SimmonsBankBaseUrl { get; set; }
public int BrowserOperationTimeoutSeconds { get; set; }
} }
public class PuppeteerConfigConstants public class PuppeteerConfigConstants
@ -12,4 +14,6 @@ public class PuppeteerConfigConstants
public const string KeepAliveIntervalMinutes = "KeepAliveIntervalMinutes"; public const string KeepAliveIntervalMinutes = "KeepAliveIntervalMinutes";
public const string CheckForNewDataIntervalMinutes = "CheckForNewDataIntervalMinutes"; public const string CheckForNewDataIntervalMinutes = "CheckForNewDataIntervalMinutes";
public const string TaskCheckIntervalMinutes = "TaskCheckIntervalMinutes"; public const string TaskCheckIntervalMinutes = "TaskCheckIntervalMinutes";
public const string SimmonsBankBaseUrl = "SimmonsBankBaseUrl";
public const string BrowserOperationTimeoutSeconds = "BrowserOperationTimeoutSeconds";
} }

View File

@ -6,5 +6,6 @@ namespace AAIntegration.SimmonsBank.API.Processes;
public interface IPuppeteerProcess public interface IPuppeteerProcess
{ {
void SetStoppingToken(CancellationToken stoppingToken);
Task StayLoggedIn(User user); Task StayLoggedIn(User user);
} }

View File

@ -8,6 +8,9 @@ using AAIntegration.SimmonsBank.API.Services;
using Internal; using Internal;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using PuppeteerSharp;
using OtpNet;
using System.Text;
namespace AAIntegration.SimmonsBank.API.Processes; namespace AAIntegration.SimmonsBank.API.Processes;
@ -16,6 +19,9 @@ public class PuppeteerProcess : IPuppeteerProcess
private readonly PuppeteerConfig _config; private readonly PuppeteerConfig _config;
private readonly ILogger<PuppeteerProcess> _logger; private readonly ILogger<PuppeteerProcess> _logger;
private readonly IMemoryCache _memoryCache; 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, IOptions<PuppeteerConfig> config,
@ -25,14 +31,192 @@ public class PuppeteerProcess : IPuppeteerProcess
_config = config.Value; _config = config.Value;
_logger = logger; _logger = logger;
_memoryCache = memoryCache; _memoryCache = memoryCache;
_stoppingToken = new CancellationToken();
}
public void SetStoppingToken(CancellationToken stoppingToken)
{
_stoppingToken = stoppingToken;
} }
public async Task StayLoggedIn(User user) public async Task StayLoggedIn(User user)
{ {
_logger.LogInformation($"... doing work and processing for user {user.Id} ..."); _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 // 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;
}
}

View File

@ -41,6 +41,7 @@ public class PuppeteerWorker : BackgroundService
using (var scope = _serviceScopeFactory.CreateScope()) using (var scope = _serviceScopeFactory.CreateScope())
{ {
_userService = scope.ServiceProvider.GetService<IUserService>(); _userService = scope.ServiceProvider.GetService<IUserService>();
_puppeteerProcess.SetStoppingToken(stoppingToken);
// This is how we keep the app running (in the background) // This is how we keep the app running (in the background)
while (!stoppingToken.IsCancellationRequested) 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)."); //_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); await _puppeteerProcess.StayLoggedIn(user);
SetUserLastExecution(PuppeteerConfigConstants.KeepAliveIntervalMinutes, user.Id, DateTime.UtcNow); SetUserLastExecution(PuppeteerConfigConstants.KeepAliveIntervalMinutes, user.Id, DateTime.UtcNow);

View File

@ -26,8 +26,10 @@
"APIUrl": "https://localhost:7260" "APIUrl": "https://localhost:7260"
}, },
"Puppeteer": { "Puppeteer": {
"BrowserOperationTimeoutSeconds": 5,
"TaskCheckIntervalMinutes": 1, "TaskCheckIntervalMinutes": 1,
"KeepAliveIntervalMinutes": 10, "KeepAliveIntervalMinutes": 1,
"CheckForNewDataIntervalMinutes": 15 "CheckForNewDataIntervalMinutes": 15,
"SimmonsBankBaseUrl": "https://login.simmonsbank.com"
} }
} }

View File

@ -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. 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 ## Dev Environment Setup
On Archlinux install the following to use dotnet: ```sudo pacman -Sy dotnet-sdk dotnet-runtime aspnet-runtime```. On Archlinux install the following to use dotnet: ```sudo pacman -Sy dotnet-sdk dotnet-runtime aspnet-runtime```.