diff --git a/AAIntegration.SimmonsBank.API/AAIntegration.SimmonsBank.API.csproj b/AAIntegration.SimmonsBank.API/AAIntegration.SimmonsBank.API.csproj
index 9617359..c6baf26 100644
--- a/AAIntegration.SimmonsBank.API/AAIntegration.SimmonsBank.API.csproj
+++ b/AAIntegration.SimmonsBank.API/AAIntegration.SimmonsBank.API.csproj
@@ -30,6 +30,7 @@
+
diff --git a/AAIntegration.SimmonsBank.API/Configs/PuppeteerConfig.cs b/AAIntegration.SimmonsBank.API/Configs/PuppeteerConfig.cs
index 907eaea..edd269c 100644
--- a/AAIntegration.SimmonsBank.API/Configs/PuppeteerConfig.cs
+++ b/AAIntegration.SimmonsBank.API/Configs/PuppeteerConfig.cs
@@ -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";
}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Processes/IPuppeteerProcess.cs b/AAIntegration.SimmonsBank.API/Processes/IPuppeteerProcess.cs
index 714f270..4313528 100644
--- a/AAIntegration.SimmonsBank.API/Processes/IPuppeteerProcess.cs
+++ b/AAIntegration.SimmonsBank.API/Processes/IPuppeteerProcess.cs
@@ -6,5 +6,6 @@ namespace AAIntegration.SimmonsBank.API.Processes;
public interface IPuppeteerProcess
{
+ void SetStoppingToken(CancellationToken stoppingToken);
Task StayLoggedIn(User user);
}
diff --git a/AAIntegration.SimmonsBank.API/Processes/PuppeteerProcess.cs b/AAIntegration.SimmonsBank.API/Processes/PuppeteerProcess.cs
index e00ed65..83ab597 100644
--- a/AAIntegration.SimmonsBank.API/Processes/PuppeteerProcess.cs
+++ b/AAIntegration.SimmonsBank.API/Processes/PuppeteerProcess.cs
@@ -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 _logger;
- private readonly IMemoryCache _memoryCache;
+ private readonly PuppeteerConfig _config;
+ private readonly ILogger _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 config,
ILogger 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 GetUserBrowserAsync(User user)
+ {
+ if (_memoryCache.TryGetValue>(BROWSER_CACHE_KEY, out var internalKeys))
+ {
+ List> 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();
+ internalKeys.Add(user.Id, browser);
+ _memoryCache.Set(BROWSER_CACHE_KEY, internalKeys);
+
+ return browser;
+ }
+
+ private async Task 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 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;
+ }
}
diff --git a/AAIntegration.SimmonsBank.API/Workers/PuppeteerWorker.cs b/AAIntegration.SimmonsBank.API/Workers/PuppeteerWorker.cs
index 9017a00..305ad20 100644
--- a/AAIntegration.SimmonsBank.API/Workers/PuppeteerWorker.cs
+++ b/AAIntegration.SimmonsBank.API/Workers/PuppeteerWorker.cs
@@ -41,6 +41,7 @@ public class PuppeteerWorker : BackgroundService
using (var scope = _serviceScopeFactory.CreateScope())
{
_userService = scope.ServiceProvider.GetService();
+ _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);
diff --git a/AAIntegration.SimmonsBank.API/appsettings.json b/AAIntegration.SimmonsBank.API/appsettings.json
index 1443388..f7948a2 100644
--- a/AAIntegration.SimmonsBank.API/appsettings.json
+++ b/AAIntegration.SimmonsBank.API/appsettings.json
@@ -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"
}
}
diff --git a/README.md b/README.md
index 7044722..e4f0f5a 100644
--- a/README.md
+++ b/README.md
@@ -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```.