Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Files.App.Storage/Ftp/FtpClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using FluentFTP;
using System.Net;
using System.Threading.Tasks;

namespace Files.App.Storage
{
Expand All @@ -13,14 +14,15 @@ public static class FtpClientFactory
{
/// <param name="url">The parsed location.</param>
/// <param name="explicitCredential">A credential the user entered for this item; otherwise the one cached for the exact scheme+host+port.</param>
public static AsyncFtpClient Create(FtpUrl url, NetworkCredential? explicitCredential = null)
public static async Task<AsyncFtpClient> CreateAsync(FtpUrl url, NetworkCredential? explicitCredential = null)
{
// Credentials embedded in the URL are kept (session only) but never override a saved one for this scope.
FtpManager.RememberUrlCredential(url);

// Only the exact scheme+host+port key is consulted; there is no host-only fallback.
// The saved credential may need the keyring unlocked, so the lookup is awaited rather than blocked on.
var credentials = explicitCredential
?? (FtpManager.Credentials.TryGetValue(url.GetCredentialKey(), out var known) ? known : FtpManager.Anonymous);
?? (await FtpManager.Credentials.TryGetSavedAsync(url.GetCredentialKey()).ConfigureAwait(false) is { } known ? known : FtpManager.Anonymous);
var isAnonymous = ReferenceEquals(credentials, FtpManager.Anonymous);

var client = new AsyncFtpClient(url.Host, credentials, url.Port);
Expand Down
4 changes: 2 additions & 2 deletions src/Files.App.Storage/Ftp/FtpHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static bool IsSameFtpPath(string firstPath, string secondPath)
string.Equals(GetFtpPath(firstPath), GetFtpPath(secondPath), StringComparison.Ordinal);
}

public static AsyncFtpClient GetFtpClient(string ftpPath)
=> FtpClientFactory.Create(FtpUrl.Parse(ftpPath));
public static Task<AsyncFtpClient> GetFtpClientAsync(string ftpPath)
=> FtpClientFactory.CreateAsync(FtpUrl.Parse(ftpPath));
}
}
63 changes: 41 additions & 22 deletions src/Files.App.Storage/Ftp/FtpManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace Files.App.Storage
{
public static class FtpManager
{
/// <summary>
/// Credentials by host. Setting an entry also saves the password in the secret store, when one is configured.
/// Credentials by host. <see cref="FtpCredentialCache.SetAsync"/> also saves the password in the secret store, when one is configured.
/// </summary>
public static readonly FtpCredentialCache Credentials = new();

Expand Down Expand Up @@ -72,10 +73,7 @@ public sealed class FtpCredentialCache : IReadOnlyDictionary<string, NetworkCred
private readonly object _lock = new();

public NetworkCredential this[string host]
{
get => TryGetValue(host, out var credential) ? credential : throw new KeyNotFoundException();
set => Set(host, value, persist: true);
}
=> TryGetValue(host, out var credential) ? credential : throw new KeyNotFoundException();

/// <summary>
/// Keeps credentials taken from a URL for this session only. They are never persisted and never override a saved
Expand All @@ -90,7 +88,10 @@ public void SetFromUrl(string credentialKey, NetworkCredential credential)
public NetworkCredential Get(string host, NetworkCredential defaultValue)
=> TryGetValue(host, out var credential) ? credential : defaultValue;

public void Remove(string host)
/// <summary>
/// Removes the credential for <paramref name="host"/> from the session and the secret store.
/// </summary>
public async Task RemoveAsync(string host)
{
lock (_lock)
{
Expand All @@ -103,9 +104,9 @@ public void Remove(string host)
try
{
var resource = GetResource(host);
if (store.Get(resource, LastUserAccount) is { } user)
store.Delete(resource, user);
store.Delete(resource, LastUserAccount);
if (await store.GetAsync(resource, LastUserAccount).ConfigureAwait(false) is { } user)
await store.DeleteAsync(resource, user).ConfigureAwait(false);
await store.DeleteAsync(resource, LastUserAccount).ConfigureAwait(false);
}
catch (Exception)
{
Expand All @@ -114,14 +115,33 @@ public void Remove(string host)
}
}

/// <summary>
/// Looks up the credential held in memory for this session; the secret store is not consulted, see <see cref="TryGetSavedAsync"/>.
/// </summary>
public bool TryGetValue(string host, out NetworkCredential value)
{
lock (_lock)
{
if (_session.TryGetValue(host, out value!))
if (_session.TryGetValue(host, out value!) || _fromUrl.TryGetValue(host, out value!))
return true;
}

value = null!;
return false;
}

/// <summary>
/// Finds the credential for <paramref name="host"/>: this session's, then the saved one, then one taken from a URL.
/// Reading the secret store may wait for the user to unlock the keyring, so this never blocks.
/// </summary>
public async Task<NetworkCredential?> TryGetSavedAsync(string host)
{
lock (_lock)
{
if (_session.TryGetValue(host, out var session))
return session;
}

// Legacy bare-host keys have no scheme/port scope and are never persisted or read from the store
var scoped = host.Contains("://", StringComparison.Ordinal);

Expand All @@ -130,11 +150,11 @@ public bool TryGetValue(string host, out NetworkCredential value)
try
{
var resource = GetResource(host);
if (store.Get(resource, LastUserAccount) is { Length: > 0 } user && store.Get(resource, user) is { } password)
if (await store.GetAsync(resource, LastUserAccount).ConfigureAwait(false) is { Length: > 0 } user
&& await store.GetAsync(resource, user).ConfigureAwait(false) is { } password)
{
// Not cached: the password stays in memory only as long as the caller needs it
value = new NetworkCredential(user, password);
return true;
return new NetworkCredential(user, password);
}
}
catch (Exception)
Expand All @@ -145,29 +165,28 @@ public bool TryGetValue(string host, out NetworkCredential value)

lock (_lock)
{
if (_fromUrl.TryGetValue(host, out value!))
return true;
return _fromUrl.TryGetValue(host, out var fromUrl) ? fromUrl : null;
}

value = null!;
return false;
}

private void Set(string host, NetworkCredential credential, bool persist)
/// <summary>
/// Keeps <paramref name="credential"/> for this session and saves it in the secret store when one is configured.
/// </summary>
public async Task SetAsync(string host, NetworkCredential credential)
{
lock (_lock)
{
_session[host] = credential;
}

if (!persist || !host.Contains("://", StringComparison.Ordinal) || FtpManager.SecretStore is not { } store || string.IsNullOrEmpty(credential.UserName))
if (!host.Contains("://", StringComparison.Ordinal) || FtpManager.SecretStore is not { } store || string.IsNullOrEmpty(credential.UserName))
return;

try
{
var resource = GetResource(host);
store.Save(resource, credential.UserName, credential.Password);
store.Save(resource, LastUserAccount, credential.UserName);
await store.SaveAsync(resource, credential.UserName, credential.Password).ConfigureAwait(false);
await store.SaveAsync(resource, LastUserAccount, credential.UserName).ConfigureAwait(false);
}
catch (Exception)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Files.App.Storage/Ftp/FtpStorable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ protected internal FtpStorable(string path, string name, IFolder? parent)
return Task.FromResult(Parent);
}

protected AsyncFtpClient GetFtpClient()
protected Task<AsyncFtpClient> GetFtpClientAsync()
{
return FtpHelpers.GetFtpClient(Id);
return FtpHelpers.GetFtpClientAsync(Id);
}
}
}
2 changes: 1 addition & 1 deletion src/Files.App.Storage/Ftp/FtpStorageFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public FtpStorageFile(string path, string name, IFolder? parent)
/// <inheritdoc/>
public async Task<Stream> OpenStreamAsync(FileAccess access, CancellationToken cancellationToken = default)
{
var ftpClient = GetFtpClient();
var ftpClient = await GetFtpClientAsync();
try
{
await ftpClient.EnsureConnectedAsync(cancellationToken);
Expand Down
10 changes: 5 additions & 5 deletions src/Files.App.Storage/Ftp/FtpStorageFolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public FtpStorageFolder(string path, string name, IFolder? parent)
/// <inheritdoc/>
public async Task<IStorableChild> GetFirstByNameAsync(string folderName, CancellationToken cancellationToken = default)
{
using var ftpClient = GetFtpClient();
using var ftpClient = await GetFtpClientAsync();
await ftpClient.EnsureConnectedAsync(cancellationToken);

var itemId = $"{Id.TrimEnd('/')}/{folderName}";
Expand All @@ -37,7 +37,7 @@ public async Task<IStorableChild> GetFirstByNameAsync(string folderName, Cancell
/// <inheritdoc/>
public async IAsyncEnumerable<IStorableChild> GetItemsAsync(StorableType kind = StorableType.All, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
using var ftpClient = GetFtpClient();
using var ftpClient = await GetFtpClientAsync();
await ftpClient.EnsureConnectedAsync(cancellationToken);

foreach (var item in await ftpClient.GetListing(FtpHelpers.GetFtpPath(Id), cancellationToken))
Expand All @@ -59,7 +59,7 @@ public Task<IFolderWatcher> GetFolderWatcherAsync(CancellationToken cancellation
/// <inheritdoc/>
public async Task DeleteAsync(IStorableChild item, CancellationToken cancellationToken = default)
{
using var ftpClient = GetFtpClient();
using var ftpClient = await GetFtpClientAsync();
await ftpClient.EnsureConnectedAsync(cancellationToken);

if (item is IFile locatableFile)
Expand Down Expand Up @@ -108,7 +108,7 @@ public async Task<IStorableChild> MoveFromAsync(IStorableChild itemToMove, IModi
/// <inheritdoc/>
public async Task<IChildFile> CreateFileAsync(string desiredName, bool overwrite = default, CancellationToken cancellationToken = default)
{
using var ftpClient = GetFtpClient();
using var ftpClient = await GetFtpClientAsync();
await ftpClient.EnsureConnectedAsync(cancellationToken);

var newId = $"{Id.TrimEnd('/')}/{desiredName}";
Expand Down Expand Up @@ -139,7 +139,7 @@ public async Task<IChildFile> CreateFileAsync(string desiredName, bool overwrite
/// <inheritdoc/>
public async Task<IChildFolder> CreateFolderAsync(string desiredName, bool overwrite = default, CancellationToken cancellationToken = default)
{
using var ftpClient = GetFtpClient();
using var ftpClient = await GetFtpClientAsync();
await ftpClient.EnsureConnectedAsync(cancellationToken);

var newId = $"{Id.TrimEnd('/')}/{desiredName}";
Expand Down
2 changes: 1 addition & 1 deletion src/Files.App.Storage/Ftp/FtpStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public async Task<IFile> GetFileAsync(string id, CancellationToken cancellationT
// The same parsed URL provides the credential scope, connection target and path.
private static async Task<FtpListItem?> GetObjectInfoAsync(FtpUrl url, CancellationToken cancellationToken)
{
using var ftpClient = FtpClientFactory.Create(url);
using var ftpClient = await FtpClientFactory.CreateAsync(url);
try
{
await ftpClient.EnsureConnectedAsync(cancellationToken);
Expand Down
4 changes: 2 additions & 2 deletions src/Files.App/Data/Contracts/IVersionControlService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,13 @@ public interface IVersionControlService
/// <summary>
/// Removes any stored credentials associated with the version control provider.
/// </summary>
void RemoveSavedCredentials();
Task RemoveSavedCredentialsAsync();

/// <summary>
/// Gets any stored credentials associated with the version control provider.
/// </summary>
/// <returns>The stored credential (typically a token), or an empty string if none exists.</returns>
string GetSavedCredentials();
Task<string> GetSavedCredentialsAsync();

/// <summary>
/// Initialises a new repository at the specified location.
Expand Down
12 changes: 6 additions & 6 deletions src/Files.App/Helpers/CredentialsHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ internal sealed class CredentialsHelpers
// Linux: the Secret Service (libsecret backend) through ISecretStore; PasswordVault does not exist there
private static ISecretStore? LinuxStore => OperatingSystem.IsLinux() ? Ioc.Default.GetService<ISecretStore>() : null;

public static void SavePassword(string resourceName, string username, string password)
public static async Task SavePasswordAsync(string resourceName, string username, string password)
{
if (LinuxStore is { } store)
{
store.Save(resourceName, username, password);
await store.SaveAsync(resourceName, username, password);
return;
}

Expand All @@ -23,11 +23,11 @@ public static void SavePassword(string resourceName, string username, string pas
}

// Remove saved credentials from the vault
public static void DeleteSavedPassword(string resourceName, string username)
public static async Task DeleteSavedPasswordAsync(string resourceName, string username)
{
if (LinuxStore is { } store)
{
store.Delete(resourceName, username);
await store.DeleteAsync(resourceName, username);
return;
}

Expand All @@ -37,10 +37,10 @@ public static void DeleteSavedPassword(string resourceName, string username)
vault.Remove(credential);
}

public static string GetPassword(string resourceName, string username)
public static async Task<string> GetPasswordAsync(string resourceName, string username)
{
if (LinuxStore is { } store)
return store.Get(resourceName, username) ?? string.Empty;
return await store.GetAsync(resourceName, username) ?? string.Empty;

try
{
Expand Down
2 changes: 1 addition & 1 deletion src/Files.App/Helpers/UI/UIFilesystemHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ public async static Task<StorageCredential> RequestPassword(IPasswordProtectedIt
{
// Scoped by scheme+host+port so the password is never offered to another service on the same host
var host = FtpUrl.Parse(path).GetCredentialKey();
FtpManager.Credentials[host] = new NetworkCredential(credentials.UserName, credentials.SecurePassword);
await FtpManager.Credentials.SetAsync(host, new NetworkCredential(credentials.UserName, credentials.SecurePassword));
}

return credentials;
Expand Down
4 changes: 3 additions & 1 deletion src/Files.App/Services/Git/LibGit2Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,13 @@ public async Task FetchOriginAsync(string? repositoryPath, bool reportProgress,
await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(
() => IsExecutingGitAction = Volatile.Read(ref _activeFetchCount) > 0);

// Awaited here: reading the token may wait for the user to unlock the keyring
var token = await CredentialsHelpers.GetPasswordAsync(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);

await Task.Run(() =>
{
using var repository = new Repository(repositoryPath);
var signature = repository.Config.BuildSignature(DateTimeOffset.Now);
var token = CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
var remotes = repository.Network.Remotes.ToArray();
var hasFetchFailure = false;

Expand Down
16 changes: 8 additions & 8 deletions src/Files.App/Utils/Git/GitHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ public static async Task SyncOriginAsync(string? repositoryPath, string? branchN
},
};

var token = CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
var token = await CredentialsHelpers.GetPasswordAsync(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
if (!string.IsNullOrWhiteSpace(token))
{
fetchOptions.CredentialsProvider = (url, user, cred)
Expand Down Expand Up @@ -276,11 +276,11 @@ private static async Task<GitOperationResult> PushToOriginCoreAsync(
if (signature is null)
return GitOperationResult.GenericError;

var token = CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
var token = await CredentialsHelpers.GetPasswordAsync(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
if (string.IsNullOrWhiteSpace(token))
{
await RequireGitAuthenticationAsync();
token = CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
token = await CredentialsHelpers.GetPasswordAsync(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
}

var progressRange = endPercentage - startPercentage;
Expand Down Expand Up @@ -483,7 +483,7 @@ public static async Task RequireGitAuthenticationAsync()

pending = false;

CredentialsHelpers.SavePassword(
await CredentialsHelpers.SavePasswordAsync(
GIT_RESOURCE_NAME,
GIT_RESOURCE_USERNAME,
token);
Expand Down Expand Up @@ -583,15 +583,15 @@ public static GitItemModel GetGitInformationForItem(Repository repository, strin
}

// Remove saved credentails
public static void RemoveSavedCredentials()
public static Task RemoveSavedCredentialsAsync()
{
CredentialsHelpers.DeleteSavedPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
return CredentialsHelpers.DeleteSavedPasswordAsync(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
}

// Get saved credentails
public static string GetSavedCredentials()
public static Task<string> GetSavedCredentialsAsync()
{
return CredentialsHelpers.GetPassword(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
return CredentialsHelpers.GetPasswordAsync(GIT_RESOURCE_NAME, GIT_RESOURCE_USERNAME);
}

public static async Task InitializeRepositoryAsync(string? path)
Expand Down
Loading
Loading