mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-13 06:07:30 -04:00
This is the reported leak. AuthenticationRequiredException held the live
Account, and Serilog.Exceptions writes every public property of a logged
exception into the log file - following nested objects as it goes - so
pausing auto-scan wrote the reporter's real address into a file we ask
people to attach to public issues. Their DecryptKey happened to be empty;
with activation bytes set it would have published those too.
The exception now carries an AccountSummary: masked entry and a
credentials flag, both safe to log, plus the owner-facing label behind a
method rather than a property, because reflection reads properties and
never calls methods. The constructor still takes an Account, so callers
and tests are unchanged.
The thrown message named the account too, and it reaches the log twice -
once as {Exception}, once as ExceptionDetail.Message - so it is masked
now. The GUI dialog still shows the full name and address, since that is
the owner's own screen. For the CLI, stderr is not teed into Serilog, so
that is where a headless user is told which account in full.
Two tests, one for the bug and one for the class of bug: the first logs a
real exception through the same WithExceptionDetails enricher Libation
configures and asserts no address, activation bytes, tokens, or cookies
come out. The second walks the public property graph of every exception
type in these assemblies and fails if one can reach an Account or an
Identity. Restoring the old property makes all of it fail, naming
"jade@example.com" and the path AuthenticationRequiredException.Account.
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
104 lines
2.8 KiB
C#
104 lines
2.8 KiB
C#
using ApplicationServices;
|
|
using AudibleUtilities;
|
|
using Serilog;
|
|
using System;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LibationUiBase;
|
|
|
|
/// <summary>
|
|
/// Runs background library auto-scan without opening login UI.
|
|
/// Pauses the timer after an authentication failure until the user logs in manually.
|
|
/// </summary>
|
|
public sealed class AutoScanRunner
|
|
{
|
|
private readonly Func<bool> isAutoScanEnabled;
|
|
private readonly Action pauseTimer;
|
|
private readonly Action resumeTimer;
|
|
private readonly Func<AuthenticationRequiredException, Task>? notifyAuthRequired;
|
|
|
|
private bool pausedForAuthentication;
|
|
|
|
public AutoScanRunner(
|
|
Func<bool> isAutoScanEnabled,
|
|
Action pauseTimer,
|
|
Action resumeTimer,
|
|
Func<AuthenticationRequiredException, Task>? notifyAuthRequired = null)
|
|
{
|
|
this.isAutoScanEnabled = isAutoScanEnabled;
|
|
this.pauseTimer = pauseTimer;
|
|
this.resumeTimer = resumeTimer;
|
|
this.notifyAuthRequired = notifyAuthRequired;
|
|
}
|
|
|
|
public void OnManualScanSucceeded()
|
|
{
|
|
if (!pausedForAuthentication)
|
|
return;
|
|
|
|
pausedForAuthentication = false;
|
|
if (isAutoScanEnabled())
|
|
resumeTimer();
|
|
}
|
|
|
|
public void OnAutoScanSettingChanged()
|
|
{
|
|
if (!isAutoScanEnabled())
|
|
pausedForAuthentication = false;
|
|
}
|
|
|
|
public async Task RunAsync()
|
|
{
|
|
if (!isAutoScanEnabled() || pausedForAuthentication)
|
|
return;
|
|
|
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
|
var accounts = persister.AccountsSettings
|
|
.GetAll()
|
|
.Where(a => a.LibraryScan)
|
|
.ToArray();
|
|
|
|
if (accounts.Length == 0)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await Task.Run(() => LibraryCommands.ImportAccountAsync(accounts, allowInteractiveLogin: false));
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Log.Information("Audible login attempt cancelled by user");
|
|
}
|
|
catch (Exception ex) when (AuthenticationExceptionHelper.IsAuthenticationFailure(ex))
|
|
{
|
|
// LoginFailedException and the "ADP token is null" case do not name an account, so fall back to one
|
|
// that at least carries the original failure
|
|
await pauseForAuthenticationAsync(
|
|
AuthenticationExceptionHelper.FindAuthenticationRequired(ex)
|
|
?? new AuthenticationRequiredException(account: null, message: ex.Message, innerException: ex));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, "Error invoking auto-scan");
|
|
}
|
|
}
|
|
|
|
private async Task pauseForAuthenticationAsync(AuthenticationRequiredException ex)
|
|
{
|
|
if (pausedForAuthentication)
|
|
return;
|
|
|
|
pausedForAuthentication = true;
|
|
pauseTimer();
|
|
|
|
// masked, not the label the dialog uses: log files get attached to public issue reports
|
|
Log.Warning(ex,
|
|
"Auto-scan paused: Audible login is required for {Account}. Log in with Import > Scan Library to resume background scans.",
|
|
ex.AccountInfo?.MaskedLogEntry ?? "[unknown account]");
|
|
|
|
if (notifyAuthRequired is not null)
|
|
await notifyAuthRequired(ex);
|
|
}
|
|
}
|