diff --git a/AAIntegration.SimmonsBank.API/Configs/PuppeteerConfig.cs b/AAIntegration.SimmonsBank.API/Configs/PuppeteerConfig.cs index edd269c..f43ce55 100644 --- a/AAIntegration.SimmonsBank.API/Configs/PuppeteerConfig.cs +++ b/AAIntegration.SimmonsBank.API/Configs/PuppeteerConfig.cs @@ -16,4 +16,10 @@ public class PuppeteerConfigConstants public const string TaskCheckIntervalMinutes = "TaskCheckIntervalMinutes"; public const string SimmonsBankBaseUrl = "SimmonsBankBaseUrl"; public const string BrowserOperationTimeoutSeconds = "BrowserOperationTimeoutSeconds"; +} + +public class PuppeteerConstants +{ + public const string BROWSER_CACHE_KEY = "BrowserCacheKey"; + public const string USER_SB_ID = "UserSimmonsBankId"; } \ No newline at end of file diff --git a/AAIntegration.SimmonsBank.API/Processes/IPuppeteerProcess.cs b/AAIntegration.SimmonsBank.API/Processes/IPuppeteerProcess.cs index 4313528..8e53020 100644 --- a/AAIntegration.SimmonsBank.API/Processes/IPuppeteerProcess.cs +++ b/AAIntegration.SimmonsBank.API/Processes/IPuppeteerProcess.cs @@ -6,6 +6,7 @@ namespace AAIntegration.SimmonsBank.API.Processes; public interface IPuppeteerProcess { + void SetService(IPuppeteerService puppeteerService); 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 83ab597..796e006 100644 --- a/AAIntegration.SimmonsBank.API/Processes/PuppeteerProcess.cs +++ b/AAIntegration.SimmonsBank.API/Processes/PuppeteerProcess.cs @@ -19,9 +19,9 @@ public class PuppeteerProcess : IPuppeteerProcess 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; + private IPuppeteerService _puppeteerService; public PuppeteerProcess( IOptions config, @@ -34,6 +34,11 @@ public class PuppeteerProcess : IPuppeteerProcess _stoppingToken = new CancellationToken(); } + public void SetService(IPuppeteerService puppeteerService) + { + _puppeteerService = puppeteerService; + } + public void SetStoppingToken(CancellationToken stoppingToken) { _stoppingToken = stoppingToken; @@ -43,12 +48,12 @@ public class PuppeteerProcess : IPuppeteerProcess { _logger.LogInformation($"... doing work and processing for user {user.Id} ..."); - IBrowser browser = await GetUserBrowserAsync(user); - - if (!await IsLoggedIn(user, browser)) + if (!await _puppeteerService.IsLoggedIn(user, _stoppingToken)) { - await Login(user, browser); + await _puppeteerService.Login(user, _stoppingToken); } + + await Delay(1000000000); } // Helper Functions @@ -57,166 +62,4 @@ public class PuppeteerProcess : IPuppeteerProcess { 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/Program.cs b/AAIntegration.SimmonsBank.API/Program.cs index b3da98b..9ec572f 100644 --- a/AAIntegration.SimmonsBank.API/Program.cs +++ b/AAIntegration.SimmonsBank.API/Program.cs @@ -84,6 +84,7 @@ internal class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/AAIntegration.SimmonsBank.API/Services/PuppeteerService.cs b/AAIntegration.SimmonsBank.API/Services/PuppeteerService.cs new file mode 100644 index 0000000..8538562 --- /dev/null +++ b/AAIntegration.SimmonsBank.API/Services/PuppeteerService.cs @@ -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 Login(User user, CancellationToken cancellationToken); + Task 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 _logger; + private readonly IMemoryCache _memoryCache; + private DataContext _context; + private readonly IMapper _mapper; + private readonly IOptions _appSettings; + + public PuppeteerService( + IOptions config, + ILogger logger, + IMemoryCache memoryCache, + DataContext context, + IMapper mapper, + IOptions appSettings) + { + _config = config.Value; + _logger = logger; + _memoryCache = memoryCache; + _context = context; + _mapper = mapper; + _appSettings = appSettings; + } + + public async Task 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 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>(PuppeteerConstants.USER_SB_ID, 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; + } + } + }*/ + + private async Task GetUserBrowserAsync(User user, CancellationToken cancellationToken) + { + if (_memoryCache.TryGetValue>(PuppeteerConstants.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().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(); + internalKeys.Add(user.Id, browser); + _memoryCache.Set(PuppeteerConstants.BROWSER_CACHE_KEY, internalKeys); + + return browser; + } +} \ No newline at end of file diff --git a/AAIntegration.SimmonsBank.API/Workers/PuppeteerWorker.cs b/AAIntegration.SimmonsBank.API/Workers/PuppeteerWorker.cs index 305ad20..561c543 100644 --- a/AAIntegration.SimmonsBank.API/Workers/PuppeteerWorker.cs +++ b/AAIntegration.SimmonsBank.API/Workers/PuppeteerWorker.cs @@ -32,6 +32,7 @@ public class PuppeteerWorker : BackgroundService } private IUserService _userService; + private IPuppeteerService _puppeteerService; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { @@ -41,7 +42,9 @@ public class PuppeteerWorker : BackgroundService using (var scope = _serviceScopeFactory.CreateScope()) { _userService = scope.ServiceProvider.GetService(); + _puppeteerService = scope.ServiceProvider.GetService(); _puppeteerProcess.SetStoppingToken(stoppingToken); + _puppeteerProcess.SetService(_puppeteerService); // This is how we keep the app running (in the background) while (!stoppingToken.IsCancellationRequested)