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; using Newtonsoft.Json.Linq; using NuGet.Protocol; using Microsoft.Extensions.Logging; using NuGet.Protocol.Core.Types; public interface IPuppeteerService { Task Login(User user, CancellationToken cancellationToken); Task IsLoggedIn(User user, CancellationToken cancellationToken); Task GetAccounts(User user, CancellationToken cancellationToken); } public class PuppeteerService : IPuppeteerService { private const string API_BASE_PATH = "/a/consumer/api"; private const string DASHBOARD_SELECTOR = "body > banno-web > bannoweb-layout > bannoweb-dashboard"; private const string USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"; private readonly PuppeteerConfig _config; private readonly ILogger _logger; private readonly IMemoryCache _memoryCache; private DataContext _context; private readonly IMapper _mapper; private readonly IOptions _appSettings; private readonly ICacheService _cacheService; public PuppeteerService( IOptions config, ILogger logger, IMemoryCache memoryCache, DataContext context, IMapper mapper, IOptions appSettings, ICacheService cacheService) { _config = config.Value; _logger = logger; _memoryCache = memoryCache; _context = context; _mapper = mapper; _appSettings = appSettings; _cacheService = cacheService; } 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.SetUserAgentAsync(USER_AGENT); await page.SetViewportAsync(new ViewPortOptions { Width = 1200, Height = 720 }); WaitUntilNavigation[] waitUntils = { WaitUntilNavigation.Networkidle0 }; // Navigate to login screen await page.GoToAsync(_config.SimmonsBankBaseUrl + "/login"); 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); // Setup response handling page.Response += LoginResponseHandler; async void LoginResponseHandler(object sender, ResponseCreatedEventArgs args) { //IPage page = sender as IPage; page.Response -= LoginResponseHandler; _logger.LogInformation("-----PARSING JSON-----"); JToken json = await args.Response.JsonAsync(); string userId = json["id"].Value(); _cacheService.SetCachedUserValue(user, PuppeteerConstants.USER_SB_ID, userId); } // 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 ex) { //_logger.LogWarning($"Login Task timed out for user '{user.Id}' after {timeout} seconds"); _logger.LogError(0, ex, $"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) { string prefix = $"Task::IsLoggedIn - {user.Id} - "; // Get User ID string userSbId = _cacheService.GetCachedUserValue(user, PuppeteerConstants.USER_SB_ID, ""); if (string.IsNullOrWhiteSpace(userSbId)) { _logger.LogInformation(prefix + $"User SimmonsBank ID not found. User is not logged in."); return false; } // Setup Page IBrowser browser = await GetUserBrowserAsync(user, cancellationToken); await using IPage page = await browser.NewPageAsync(); await page.SetUserAgentAsync(USER_AGENT); await page.SetViewportAsync(new ViewPortOptions { Width = 1200, Height = 720 }); // Fetch accounts string url = _config.SimmonsBankBaseUrl + API_BASE_PATH + "/users/" + userSbId + "/accounts"; try { IResponse response = await page.GoToAsync(url).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds), cancellationToken); _logger.LogInformation(prefix + $"Request response code '{response.Status}'. Url: '{url}'"); return response.Status == System.Net.HttpStatusCode.OK; } catch(TaskCanceledException) { _logger.LogWarning(prefix + $"Task was canceled"); } catch(TimeoutException) { _logger.LogWarning(prefix + $"Request to '{url}' timed out"); } return false; } public async Task GetAccounts(User user, CancellationToken cancellationToken) { string prefix = $"Task::GetAccounts - {user.Id} - "; // Get User ID string userSbId = _cacheService.GetCachedUserValue(user, PuppeteerConstants.USER_SB_ID, ""); if (string.IsNullOrWhiteSpace(userSbId)) { _logger.LogInformation(prefix + $"User SimmonsBank ID not found. User is not logged in."); return null; } // Setup Page IBrowser browser = await GetUserBrowserAsync(user, cancellationToken); await using IPage page = await browser.NewPageAsync(); await page.SetUserAgentAsync(USER_AGENT); await page.SetViewportAsync(new ViewPortOptions { Width = 1200, Height = 720 }); // Fetch accounts string url = _config.SimmonsBankBaseUrl + API_BASE_PATH + "/users/" + userSbId + "/accounts"; try { IResponse response = await page.GoToAsync(url).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds), cancellationToken); _logger.LogInformation(prefix + $"Request response code '{response.Status}'. Url: '{url}'"); if (response.Status == System.Net.HttpStatusCode.OK) return await response.JsonAsync(); else _logger.LogError(prefix + $"Received unexpected status code '{response.Status}'"); } catch(TaskCanceledException) { _logger.LogWarning(prefix + $"Task was canceled"); } catch(TimeoutException) { _logger.LogWarning(prefix + $"Request to '{url}' timed out"); } return null; } // Helper / Private Functions private async Task GetUserBrowserAsync(User user, CancellationToken cancellationToken) { IBrowser cachedBrowser = _cacheService.GetCachedUserValue(user, PuppeteerConstants.BROWSER_CACHE_KEY, null); if (cachedBrowser != null) return cachedBrowser; _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 = true, IgnoreHTTPSErrors = true, }; IBrowser browser = await Puppeteer.LaunchAsync(options).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds), cancellationToken); _cacheService.SetCachedUserValue(user, PuppeteerConstants.BROWSER_CACHE_KEY, browser); return browser; } }