Switched to headless mode for puppeteer. Refactored caching, added GetAccounts method.
This commit is contained in:
parent
436c1baaf7
commit
48958be669
@ -50,10 +50,13 @@ public class PuppeteerProcess : IPuppeteerProcess
|
|||||||
|
|
||||||
if (!await _puppeteerService.IsLoggedIn(user, _stoppingToken))
|
if (!await _puppeteerService.IsLoggedIn(user, _stoppingToken))
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("User determined to not be logged in");
|
||||||
await _puppeteerService.Login(user, _stoppingToken);
|
await _puppeteerService.Login(user, _stoppingToken);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
await Delay(1000000000);
|
{
|
||||||
|
_logger.LogInformation("User is already logged in");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
|
@ -1,22 +1,29 @@
|
|||||||
namespace AAIntegration.SimmonsBank.API.Services;
|
namespace AAIntegration.SimmonsBank.API.Services;
|
||||||
|
|
||||||
|
using AAIntegration.SimmonsBank.API.Config;
|
||||||
|
using AAIntegration.SimmonsBank.API.Entities;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
public interface ICacheService
|
public interface ICacheService
|
||||||
{
|
{
|
||||||
int GetClientIdFromApiKey(string apiKey);
|
int GetClientIdFromApiKey(string apiKey);
|
||||||
|
T GetCachedUserValue<T>(User user, string cacheKey, T fallback);
|
||||||
|
void SetCachedUserValue<T>(User user, string cacheKey, T value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheService : ICacheService
|
public class CacheService : ICacheService
|
||||||
{
|
{
|
||||||
|
private DataContext _context;
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly IUserService _userService;
|
|
||||||
private readonly ILogger<ICacheService> _logger;
|
private readonly ILogger<ICacheService> _logger;
|
||||||
|
|
||||||
public CacheService(IMemoryCache memoryCache, IUserService userService, ILogger<ICacheService> logger)
|
public CacheService(
|
||||||
|
DataContext context,
|
||||||
|
IMemoryCache memoryCache,
|
||||||
|
ILogger<ICacheService> logger)
|
||||||
{
|
{
|
||||||
|
_context = context;
|
||||||
_memoryCache = memoryCache;
|
_memoryCache = memoryCache;
|
||||||
_userService = userService;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +33,9 @@ public class CacheService : ICacheService
|
|||||||
{
|
{
|
||||||
_logger.LogInformation($"Could not find API key '{apiKey}' in cache.");
|
_logger.LogInformation($"Could not find API key '{apiKey}' in cache.");
|
||||||
|
|
||||||
internalKeys = _userService.GetAllApiKeys();
|
internalKeys = _context.Users
|
||||||
|
.Where(u => u.ApiKey != null)
|
||||||
|
.ToDictionary(u => u.ApiKey, u => u.Id);
|
||||||
|
|
||||||
_logger.LogInformation("Updated cache with new key list.");
|
_logger.LogInformation("Updated cache with new key list.");
|
||||||
PrintInternalKeys(internalKeys);
|
PrintInternalKeys(internalKeys);
|
||||||
@ -43,6 +52,36 @@ public class CacheService : ICacheService
|
|||||||
return clientId;
|
return clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public T GetCachedUserValue<T>(User user, string cacheKey, T fallback)
|
||||||
|
{
|
||||||
|
if (_memoryCache.TryGetValue<Dictionary<int, T>>(cacheKey, out var internalKeys))
|
||||||
|
{
|
||||||
|
internalKeys ??= new Dictionary<int, T>();
|
||||||
|
List<KeyValuePair<int, T>> list = internalKeys.Where(x => x.Key == user.Id).ToList();
|
||||||
|
|
||||||
|
if (list.Count > 0 && list.First().Value != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Found the '{typeof(T)}' type cached for user with id '{user.Id}' in cache '{cacheKey}'.");
|
||||||
|
return list.First().Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCachedUserValue<T>(User user, string cacheKey, T value)
|
||||||
|
{
|
||||||
|
_memoryCache.TryGetValue<Dictionary<int, T>>(cacheKey, out var internalKeys);
|
||||||
|
internalKeys ??= new Dictionary<int, T>();
|
||||||
|
|
||||||
|
if (internalKeys.ContainsKey(user.Id))
|
||||||
|
internalKeys[user.Id] = value;
|
||||||
|
else
|
||||||
|
internalKeys.Add(user.Id, value);
|
||||||
|
|
||||||
|
_memoryCache.Set(cacheKey, internalKeys);
|
||||||
|
}
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
private void PrintInternalKeys(Dictionary<string, int> keys)
|
private void PrintInternalKeys(Dictionary<string, int> keys)
|
||||||
|
@ -19,22 +19,30 @@ using PuppeteerSharp;
|
|||||||
using AAIntegration.SimmonsBank.API.Configs;
|
using AAIntegration.SimmonsBank.API.Configs;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using OtpNet;
|
using OtpNet;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NuGet.Protocol;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NuGet.Protocol.Core.Types;
|
||||||
|
|
||||||
public interface IPuppeteerService
|
public interface IPuppeteerService
|
||||||
{
|
{
|
||||||
Task<bool> Login(User user, CancellationToken cancellationToken);
|
Task<bool> Login(User user, CancellationToken cancellationToken);
|
||||||
Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken);
|
Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken);
|
||||||
|
Task<JToken> GetAccounts(User user, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PuppeteerService : IPuppeteerService
|
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 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 PuppeteerConfig _config;
|
||||||
private readonly ILogger<PuppeteerService> _logger;
|
private readonly ILogger<PuppeteerService> _logger;
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
private DataContext _context;
|
private DataContext _context;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly IOptions<AppSettings> _appSettings;
|
private readonly IOptions<AppSettings> _appSettings;
|
||||||
|
private readonly ICacheService _cacheService;
|
||||||
|
|
||||||
public PuppeteerService(
|
public PuppeteerService(
|
||||||
IOptions<PuppeteerConfig> config,
|
IOptions<PuppeteerConfig> config,
|
||||||
@ -42,7 +50,8 @@ public class PuppeteerService : IPuppeteerService
|
|||||||
IMemoryCache memoryCache,
|
IMemoryCache memoryCache,
|
||||||
DataContext context,
|
DataContext context,
|
||||||
IMapper mapper,
|
IMapper mapper,
|
||||||
IOptions<AppSettings> appSettings)
|
IOptions<AppSettings> appSettings,
|
||||||
|
ICacheService cacheService)
|
||||||
{
|
{
|
||||||
_config = config.Value;
|
_config = config.Value;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -50,6 +59,7 @@ public class PuppeteerService : IPuppeteerService
|
|||||||
_context = context;
|
_context = context;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
_appSettings = appSettings;
|
_appSettings = appSettings;
|
||||||
|
_cacheService = cacheService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> Login(User user, CancellationToken cancellationToken)
|
public async Task<bool> Login(User user, CancellationToken cancellationToken)
|
||||||
@ -59,11 +69,12 @@ public class PuppeteerService : IPuppeteerService
|
|||||||
// Setup Page
|
// Setup Page
|
||||||
IBrowser browser = await GetUserBrowserAsync(user, cancellationToken);
|
IBrowser browser = await GetUserBrowserAsync(user, cancellationToken);
|
||||||
await using IPage page = await browser.NewPageAsync();
|
await using IPage page = await browser.NewPageAsync();
|
||||||
|
await page.SetUserAgentAsync(USER_AGENT);
|
||||||
await page.SetViewportAsync(new ViewPortOptions { Width = 1200, Height = 720 });
|
await page.SetViewportAsync(new ViewPortOptions { Width = 1200, Height = 720 });
|
||||||
WaitUntilNavigation[] waitUntils = { WaitUntilNavigation.Networkidle0 };
|
WaitUntilNavigation[] waitUntils = { WaitUntilNavigation.Networkidle0 };
|
||||||
|
|
||||||
// Navigate to login screen
|
// Navigate to login screen
|
||||||
await page.GoToAsync(_config.SimmonsBankBaseUrl + "/login");//, null, waitUntils); // wait until page load
|
await page.GoToAsync(_config.SimmonsBankBaseUrl + "/login");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -112,6 +123,19 @@ public class PuppeteerService : IPuppeteerService
|
|||||||
IElementHandle totpInput = await page.QuerySelectorAsync(selector);
|
IElementHandle totpInput = await page.QuerySelectorAsync(selector);
|
||||||
await totpInput.TypeAsync(totpCode);
|
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<JToken>();
|
||||||
|
string userId = json["id"].Value<string>();
|
||||||
|
_cacheService.SetCachedUserValue<string>(user, PuppeteerConstants.USER_SB_ID, userId);
|
||||||
|
}
|
||||||
|
|
||||||
// Click Verify Button
|
// 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";
|
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);
|
await page.WaitForSelectorAsync(selector).WaitAsync(timeout, cancellationToken);
|
||||||
@ -126,7 +150,6 @@ public class PuppeteerService : IPuppeteerService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await page.WaitForSelectorAsync(DASHBOARD_SELECTOR).WaitAsync(timeout, cancellationToken);
|
await page.WaitForSelectorAsync(DASHBOARD_SELECTOR).WaitAsync(timeout, cancellationToken);
|
||||||
@ -144,9 +167,10 @@ public class PuppeteerService : IPuppeteerService
|
|||||||
{
|
{
|
||||||
_logger.LogError($"Login Task for user '{user.Id}' was canceled");
|
_logger.LogError($"Login Task for user '{user.Id}' was canceled");
|
||||||
}
|
}
|
||||||
catch (TimeoutException)
|
catch (TimeoutException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning($"Login Task timed out for user '{user.Id}' after {timeout} seconds");
|
//_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;
|
return false;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -160,58 +184,95 @@ public class PuppeteerService : IPuppeteerService
|
|||||||
|
|
||||||
public async Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken)
|
public async Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Setup Page
|
string prefix = $"Task::IsLoggedIn - {user.Id} - ";
|
||||||
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
|
// Get User ID
|
||||||
await page.GoToAsync(_config.SimmonsBankBaseUrl);
|
string userSbId = _cacheService.GetCachedUserValue<string>(user, PuppeteerConstants.USER_SB_ID, "");
|
||||||
|
if (string.IsNullOrWhiteSpace(userSbId))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation(prefix + $"User SimmonsBank ID not found. User is not logged in.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// 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<JToken> GetAccounts(User user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
string prefix = $"Task::GetAccounts - {user.Id} - ";
|
||||||
|
|
||||||
|
// Get User ID
|
||||||
|
string userSbId = _cacheService.GetCachedUserValue<string>(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<JToken>();
|
||||||
|
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
|
// 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)
|
private async Task<IBrowser> GetUserBrowserAsync(User user, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_memoryCache.TryGetValue<Dictionary<int, IBrowser>>(PuppeteerConstants.BROWSER_CACHE_KEY, out var internalKeys))
|
IBrowser cachedBrowser = _cacheService.GetCachedUserValue<IBrowser>(user, PuppeteerConstants.BROWSER_CACHE_KEY, null);
|
||||||
{
|
|
||||||
List<KeyValuePair<int, IBrowser>> list = internalKeys.Where(x => x.Key == user.Id).ToList();
|
|
||||||
|
|
||||||
if (list.Count > 0 && list.First().Value != null)
|
if (cachedBrowser != null)
|
||||||
{
|
return cachedBrowser;
|
||||||
_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...");
|
_logger.LogInformation($"Could NOT find the browser for user with id '{user.Id}'. About to create one...");
|
||||||
|
|
||||||
@ -219,15 +280,13 @@ public class PuppeteerService : IPuppeteerService
|
|||||||
await browserFetcher.DownloadAsync().WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds * 20), cancellationToken);
|
await browserFetcher.DownloadAsync().WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds * 20), cancellationToken);
|
||||||
|
|
||||||
var options = new LaunchOptions {
|
var options = new LaunchOptions {
|
||||||
Headless = false,
|
Headless = true,
|
||||||
IgnoreHTTPSErrors = true
|
IgnoreHTTPSErrors = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
IBrowser browser = await Puppeteer.LaunchAsync(options).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds), cancellationToken);
|
IBrowser browser = await Puppeteer.LaunchAsync(options).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds), cancellationToken);
|
||||||
|
|
||||||
internalKeys ??= new Dictionary<int, IBrowser>();
|
_cacheService.SetCachedUserValue<IBrowser>(user, PuppeteerConstants.BROWSER_CACHE_KEY, browser);
|
||||||
internalKeys.Add(user.Id, browser);
|
|
||||||
_memoryCache.Set(PuppeteerConstants.BROWSER_CACHE_KEY, internalKeys);
|
|
||||||
|
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user