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; } }