Moved the puppeteer logic to the new PuppeteerService class

This commit is contained in:
William Lewis 2024-04-02 22:28:52 -05:00
parent 8231cedcf1
commit 436c1baaf7
6 changed files with 255 additions and 167 deletions

View File

@ -17,3 +17,9 @@ public class PuppeteerConfigConstants
public const string SimmonsBankBaseUrl = "SimmonsBankBaseUrl"; public const string SimmonsBankBaseUrl = "SimmonsBankBaseUrl";
public const string BrowserOperationTimeoutSeconds = "BrowserOperationTimeoutSeconds"; public const string BrowserOperationTimeoutSeconds = "BrowserOperationTimeoutSeconds";
} }
public class PuppeteerConstants
{
public const string BROWSER_CACHE_KEY = "BrowserCacheKey";
public const string USER_SB_ID = "UserSimmonsBankId";
}

View File

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

View File

@ -19,9 +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 const string DASHBOARD_SELECTOR = "body > banno-web > bannoweb-layout > bannoweb-dashboard";
private CancellationToken _stoppingToken; private CancellationToken _stoppingToken;
private IPuppeteerService _puppeteerService;
public PuppeteerProcess( public PuppeteerProcess(
IOptions<PuppeteerConfig> config, IOptions<PuppeteerConfig> config,
@ -34,6 +34,11 @@ public class PuppeteerProcess : IPuppeteerProcess
_stoppingToken = new CancellationToken(); _stoppingToken = new CancellationToken();
} }
public void SetService(IPuppeteerService puppeteerService)
{
_puppeteerService = puppeteerService;
}
public void SetStoppingToken(CancellationToken stoppingToken) public void SetStoppingToken(CancellationToken stoppingToken)
{ {
_stoppingToken = stoppingToken; _stoppingToken = stoppingToken;
@ -43,12 +48,12 @@ public class PuppeteerProcess : IPuppeteerProcess
{ {
_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 _puppeteerService.IsLoggedIn(user, _stoppingToken))
if (!await IsLoggedIn(user, browser))
{ {
await Login(user, browser); await _puppeteerService.Login(user, _stoppingToken);
} }
await Delay(1000000000);
} }
// Helper Functions // Helper Functions
@ -57,166 +62,4 @@ public class PuppeteerProcess : IPuppeteerProcess
{ {
await Task.Delay(TimeSpan.FromMilliseconds(milliseconds), _stoppingToken); 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

@ -84,6 +84,7 @@ internal class Program
builder.Services.AddScoped<ITransactionService, TransactionService>(); builder.Services.AddScoped<ITransactionService, TransactionService>();
builder.Services.AddScoped<ICacheService, CacheService>(); builder.Services.AddScoped<ICacheService, CacheService>();
builder.Services.AddScoped<IVersionService, VersionService>(); builder.Services.AddScoped<IVersionService, VersionService>();
builder.Services.AddScoped<IPuppeteerService, PuppeteerService>();
builder.Services.AddScoped<ApiKeyAuthenticationHandler>(); builder.Services.AddScoped<ApiKeyAuthenticationHandler>();

View File

@ -0,0 +1,234 @@
namespace AAIntegration.SimmonsBank.API.Services;
using AutoMapper;
using BCrypt.Net;
using AAIntegration.SimmonsBank.API.Entities;
using AAIntegration.SimmonsBank.API.Config;
using AAIntegration.SimmonsBank.API.Models.Users;
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.Extensions.Options;
using System.Security.Cryptography;
using PuppeteerSharp;
using AAIntegration.SimmonsBank.API.Configs;
using Microsoft.Extensions.Caching.Memory;
using OtpNet;
public interface IPuppeteerService
{
Task<bool> Login(User user, CancellationToken cancellationToken);
Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken);
}
public class PuppeteerService : IPuppeteerService
{
private const string DASHBOARD_SELECTOR = "body > banno-web > bannoweb-layout > bannoweb-dashboard";
private readonly PuppeteerConfig _config;
private readonly ILogger<PuppeteerService> _logger;
private readonly IMemoryCache _memoryCache;
private DataContext _context;
private readonly IMapper _mapper;
private readonly IOptions<AppSettings> _appSettings;
public PuppeteerService(
IOptions<PuppeteerConfig> config,
ILogger<PuppeteerService> logger,
IMemoryCache memoryCache,
DataContext context,
IMapper mapper,
IOptions<AppSettings> appSettings)
{
_config = config.Value;
_logger = logger;
_memoryCache = memoryCache;
_context = context;
_mapper = mapper;
_appSettings = appSettings;
}
public async Task<bool> Login(User user, CancellationToken cancellationToken)
{
TimeSpan timeout = TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds);
// Setup Page
IBrowser browser = await GetUserBrowserAsync(user, cancellationToken);
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, cancellationToken);
await page.TypeAsync(selector, user.SimmonsBankUsername);
// Press 1st Submit Button
selector = "jha-button";
await page.WaitForSelectorAsync(selector).WaitAsync(timeout, cancellationToken);
await page.ClickAsync(selector);
// Type password
selector = "#password";
await page.WaitForSelectorAsync(selector).WaitAsync(timeout, cancellationToken);
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, cancellationToken);
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, cancellationToken);
//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, cancellationToken);
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, cancellationToken);
}
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;
}
public async Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken)
{
// Setup Page
IBrowser browser = await GetUserBrowserAsync(user, cancellationToken);
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), cancellationToken);
}
catch(TaskCanceledException)
{
_logger.LogWarning($"IsLoggedIn Task for user '{user.Id}' was canceled");
}
catch(TimeoutException)
{
return false;
}
return true;
}
// Helper / Private Functions
/*private void SetUserSBId(User user)
{
if (_memoryCache.TryGetValue<Dictionary<int, string>>(PuppeteerConstants.USER_SB_ID, 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;
}
}
}*/
private async Task<IBrowser> GetUserBrowserAsync(User user, CancellationToken cancellationToken)
{
if (_memoryCache.TryGetValue<Dictionary<int, IBrowser>>(PuppeteerConstants.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().WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds * 20), cancellationToken);
var options = new LaunchOptions {
Headless = false,
IgnoreHTTPSErrors = true
};
IBrowser browser = await Puppeteer.LaunchAsync(options).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds), cancellationToken);
internalKeys ??= new Dictionary<int, IBrowser>();
internalKeys.Add(user.Id, browser);
_memoryCache.Set(PuppeteerConstants.BROWSER_CACHE_KEY, internalKeys);
return browser;
}
}

View File

@ -32,6 +32,7 @@ public class PuppeteerWorker : BackgroundService
} }
private IUserService _userService; private IUserService _userService;
private IPuppeteerService _puppeteerService;
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
@ -41,7 +42,9 @@ public class PuppeteerWorker : BackgroundService
using (var scope = _serviceScopeFactory.CreateScope()) using (var scope = _serviceScopeFactory.CreateScope())
{ {
_userService = scope.ServiceProvider.GetService<IUserService>(); _userService = scope.ServiceProvider.GetService<IUserService>();
_puppeteerService = scope.ServiceProvider.GetService<IPuppeteerService>();
_puppeteerProcess.SetStoppingToken(stoppingToken); _puppeteerProcess.SetStoppingToken(stoppingToken);
_puppeteerProcess.SetService(_puppeteerService);
// 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)