This fixes some issues with URL encoding/decoding. Due to the legacy class `Duplicati.Library.Utility.Uri` there are two replacements done.
193 lines
7.8 KiB
C#
193 lines
7.8 KiB
C#
using System.Text.Json;
|
|
using Duplicati.Library.DynamicLoader;
|
|
using Duplicati.Library.Encryption;
|
|
using Duplicati.Library.Interface;
|
|
using Duplicati.Library.Main;
|
|
using Duplicati.Library.Utility;
|
|
|
|
namespace Duplicati.CommandLine.ServerUtil;
|
|
|
|
/// <summary>
|
|
/// Settings instance for the server utility.
|
|
/// </summary>
|
|
/// <param name="Password">The commandline password</param>
|
|
/// <param name="RefreshToken">The saved refresh token</param>
|
|
/// <param name="HostUrl">The host url to connect to</param>
|
|
/// <param name="ServerDatafolder">The server datafolder for password-free connections</param>
|
|
/// <param name="SettingsFile">The settings file where data is loaded/saved</param>
|
|
/// <param name="Insecure">Whether to disable TLS/SSL certificate trust check</param>
|
|
/// <param name="Key">The encryption key to use for the settings file</param>
|
|
/// <param name="SecretProvider">The secret provider to use for reading secrets</param>
|
|
/// <param name="SecretProviderPattern">The pattern to use for the secret provider</param>
|
|
/// <param name="AcceptedHostCertificate">The SHA1 hash of the host certificate to accept</param>
|
|
public sealed record Settings(
|
|
string? Password,
|
|
string? RefreshToken,
|
|
System.Uri HostUrl,
|
|
string? ServerDatafolder,
|
|
string SettingsFile,
|
|
bool Insecure,
|
|
EncryptedFieldHelper.KeyInstance? Key,
|
|
ISecretProvider? SecretProvider,
|
|
string SecretProviderPattern,
|
|
string? AcceptedHostCertificate
|
|
)
|
|
{
|
|
/// <summary>
|
|
/// The JSON serialized settings for a single host
|
|
/// </summary>
|
|
/// <param name="RefreshToken">Encrypted refresh token</param>
|
|
/// <param name="HostUrl">The host url to connect to</param>
|
|
/// <param name="ServerDatafolder">The server datafolder, if any</param>
|
|
private sealed record PersistedSettings(
|
|
string? RefreshToken,
|
|
System.Uri HostUrl,
|
|
string? ServerDatafolder
|
|
);
|
|
private static string GetDefaultStorageFolder(string filename)
|
|
{
|
|
var folder = DatabaseLocator.GetDefaultStorageFolderWithDebugSupport(filename);
|
|
if (!Directory.Exists(folder))
|
|
Directory.CreateDirectory(folder);
|
|
|
|
return folder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the settings from the settings file
|
|
/// </summary>
|
|
/// <param name="password">The password to use</param>
|
|
/// <param name="hostUrl">The host URL to use</param>
|
|
/// <param name="serverDataFolder">The server data folder to use</param>
|
|
/// <param name="settingsFile">The settings file to use</param>
|
|
/// <param name="insecure">Whether to disable TLS/SSL certificate trust check</param>
|
|
/// <param name="settingsPassphrase">The encryption key to use</param>
|
|
/// <param name="secretProvider">The secret provider to use</param>
|
|
/// <param name="secretProviderCache">The secret provider cache level to use</param>
|
|
/// <param name="secretProviderPattern">The secret provider pattern to use</param>
|
|
/// <param name="acceptedHostCertificate">The SHA1 hash of the host certificate to accept</param>
|
|
/// <returns>The loaded settings</returns>
|
|
public static Settings Load(string? password, System.Uri? hostUrl, string? serverDataFolder, string settingsFile, bool insecure, string? settingsPassphrase, string? secretProvider, SecretProviderHelper.CachingLevel secretProviderCache, string secretProviderPattern, string? acceptedHostCertificate)
|
|
{
|
|
hostUrl ??= new System.Uri("http://localhost:8200");
|
|
|
|
ISecretProvider? secretInstance = null;
|
|
if (!string.IsNullOrWhiteSpace(secretProvider))
|
|
{
|
|
var secretProviderInstance = SecretProviderLoader.CreateInstance(secretProvider);
|
|
|
|
// Map into expected structure
|
|
var opts = new Dictionary<string, string?>
|
|
{
|
|
{ "secret-provider", secretProvider },
|
|
{ "secret-provider-pattern", secretProviderPattern },
|
|
{ "secret-provider-cache", secretProviderCache.ToString() },
|
|
{ "password", password },
|
|
{ "settings-encryption-key", settingsPassphrase }
|
|
};
|
|
|
|
var args = new[] { hostUrl };
|
|
secretInstance = SecretProviderHelper.ApplySecretProviderAsync(args, [], opts, Library.Utility.TempFolder.SystemTempPath, null, CancellationToken.None).Await();
|
|
|
|
// Read back transformed values
|
|
hostUrl = args[0];
|
|
password = opts["password"];
|
|
settingsPassphrase = opts["settings-encryption-key"];
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(serverDataFolder))
|
|
serverDataFolder = GetDefaultStorageFolder("Duplicati-server.sqlite");
|
|
|
|
if (!string.IsNullOrWhiteSpace(settingsFile) && !Path.IsPathRooted(settingsFile))
|
|
settingsFile = Path.Combine(GetDefaultStorageFolder(settingsFile), settingsFile);
|
|
|
|
var key = EncryptedFieldHelper.KeyInstance.CreateKeyIfValid(settingsPassphrase);
|
|
var persistedSettings = LoadSettings(settingsFile, key)
|
|
.FirstOrDefault(x => x.HostUrl == hostUrl);
|
|
|
|
return new Settings(
|
|
password,
|
|
persistedSettings?.RefreshToken,
|
|
hostUrl,
|
|
serverDataFolder,
|
|
settingsFile,
|
|
insecure,
|
|
key,
|
|
secretInstance,
|
|
secretProviderPattern,
|
|
acceptedHostCertificate
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replaces secrets inside arguments and options
|
|
/// </summary>
|
|
/// <param name="args">The arguments to replace</param>
|
|
/// <param name="options">The options to replace</param>
|
|
/// <returns>The task to await</returns>
|
|
public Task ReplaceSecrets(Dictionary<string, string?> options)
|
|
{
|
|
if (SecretProvider == null)
|
|
return Task.CompletedTask;
|
|
|
|
return SecretProviderHelper.ReplaceSecretsAsync(SecretProvider, [], [], options, SecretProviderPattern, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves the settings to the settings file
|
|
/// </summary>
|
|
public void Save()
|
|
{
|
|
var thisKey = Key;
|
|
if (!string.IsNullOrWhiteSpace(RefreshToken))
|
|
{
|
|
if (Key == null)
|
|
{
|
|
Console.WriteLine("Warning: The encryption key is missing, saving login token without encryption");
|
|
}
|
|
else if (Key?.IsBlacklisted ?? false)
|
|
{
|
|
Console.WriteLine("Warning: The current encryption key is blacklisted and cannot be used, saving login token without encryption");
|
|
thisKey = null;
|
|
}
|
|
|
|
}
|
|
|
|
File.WriteAllText(SettingsFile, JsonSerializer.Serialize(LoadSettings(SettingsFile, thisKey)
|
|
.Where(x => x.HostUrl != HostUrl)
|
|
.Append(new PersistedSettings(RefreshToken, HostUrl, ServerDatafolder))
|
|
.Select(x => x with
|
|
{
|
|
RefreshToken = string.IsNullOrWhiteSpace(x.RefreshToken) || thisKey == null
|
|
? x.RefreshToken
|
|
: EncryptedFieldHelper.Encrypt(x.RefreshToken, thisKey)
|
|
})
|
|
));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a connection to the server
|
|
/// </summary>
|
|
/// <returns>The connection</returns>
|
|
public Task<Connection> GetConnection()
|
|
{
|
|
return Connection.Connect(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the settings from the settings file
|
|
/// </summary>
|
|
/// <param name="filename">The filename to load</param>
|
|
/// <param name="key">The encryption key to use</param>
|
|
/// <returns>The loaded settings</returns>
|
|
private static List<PersistedSettings> LoadSettings(string filename, EncryptedFieldHelper.KeyInstance? key)
|
|
{
|
|
if (File.Exists(filename))
|
|
return (JsonSerializer.Deserialize<List<PersistedSettings>>(File.ReadAllText(filename)) ?? [])
|
|
.Select(x => x with { RefreshToken = EncryptedFieldHelper.Decrypt(x.RefreshToken, key) })
|
|
.ToList();
|
|
|
|
return [];
|
|
}
|
|
}
|