using Duplicati.Library.Interface; using Duplicati.Library.Main; using Duplicati.Library.Utility; using Duplicati.Server; using Duplicati.Server.Database; using Duplicati.WebserverCore.Abstractions; namespace Duplicati.WebserverCore.Endpoints.Shared; public record BackendTupleDisposeWrapper(IBackend Backend, IEnumerable Modules) : IDisposable { public void Dispose() { Backend?.Dispose(); foreach (var n in Modules) if (n is IDisposable disposable) disposable.Dispose(); } } public record SourceProviderTupleDisposeWrapper(ISourceProvider SourceProvider, IEnumerable Modules) : IDisposable { public void Dispose() { SourceProvider?.Dispose(); foreach (var n in Modules) if (n is IDisposable disposable) disposable.Dispose(); } } public record RestoreDestinationProviderTupleDisposeWrapper(IRestoreDestinationProvider RestoreDestinationProvider, IEnumerable Modules) : IDisposable { public void Dispose() { RestoreDestinationProvider?.Dispose(); foreach (var n in Modules) if (n is IDisposable disposable) disposable.Dispose(); } } public class SharedRemoteOperation { public static Dictionary ParseUrlOptions(Connection connection, Library.Utility.Uri uri) { var qp = uri.QueryParameters; var opts = Runner.GetCommonOptions(connection); foreach (var k in qp.Keys.Cast()) opts[k] = qp[k]; return opts; } public static IEnumerable ConfigureModules(IDictionary opts) { var modules = Library.DynamicLoader.GenericLoader.Modules.OfType() .Select(x => Library.DynamicLoader.GenericLoader.GetModule(x.Key)) .WhereNotNull() .ToArray(); foreach (var n in modules) n.Configure(opts); return modules; } /// /// This method will unmask the URL if needed, apply secrets from the secret provider, and merge in remote control storage API credentials if it's a Duplicati backend URL. /// It returns the expanded URL and the options used for the expansion, which may be needed for later operations to ensure consistency. /// /// The database connection /// The application settings /// The URL to expand /// The backup ID /// The prefix, if this is a remote source /// The cancellation token /// The unmasked URL and the options after the expansion public static async Task<(string Url, Dictionary Options)> ExpandUrl(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, string? sourcePrefix, CancellationToken cancelToken) { url = UnmaskUrl(connection, url, backupId, connectionStringId, sourcePrefix); var uri = new Library.Utility.Uri(url); var opts = ParseUrlOptions(connection, uri); var tmp = new[] { uri }; await SecretProviderHelper.ApplySecretProviderAsync([], tmp, opts, Library.Utility.TempFolder.SystemTempPath, applicationSettings.SecretProvider, cancelToken); url = tmp[0].ToString(); if (uri.Scheme.Equals(Library.Backend.Duplicati.DuplicatiBackend.PROTOCOL, StringComparison.OrdinalIgnoreCase)) url = Library.Backend.Duplicati.DuplicatiBackend.MergeArgsIntoUrl( url, connection.ApplicationSettings.RemoteControlStorageApiId, connection.ApplicationSettings.RemoteControlStorageApiKey, connection.ApplicationSettings.RemoteControlStorageEndpointUrl ); return (url, opts); } public static async Task GetBackend(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, CancellationToken cancelToken) { (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, null, cancelToken); var modules = ConfigureModules(opts); var backend = Library.DynamicLoader.BackendLoader.GetBackend(url, opts); return new BackendTupleDisposeWrapper(backend, modules); } public static async Task GetSourceProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, string? sourcePrefix, CancellationToken cancelToken) { (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, sourcePrefix, cancelToken); var modules = ConfigureModules(opts); var sourceProvider = await Library.DynamicLoader.SourceProviderLoader.GetSourceProviderForTesting(url, "", opts, cancelToken); return new SourceProviderTupleDisposeWrapper(sourceProvider, modules); } public static async Task GetRestoreDestinationProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, string? sourcePrefix, CancellationToken cancelToken) { (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, sourcePrefix, cancelToken); var modules = ConfigureModules(opts); var restoreDestinationProvider = await Library.DynamicLoader.RestoreDestinationProviderLoader.GetRestoreDestinationProviderForTesting(url, opts, cancelToken); return new RestoreDestinationProviderTupleDisposeWrapper(restoreDestinationProvider, modules); } public static Exception GetInnerException(Exception ex) where T : Exception { var original = ex; while (true) { if (ex == null) return original; if (ex is T) return (T)ex; if (ex.InnerException == null) throw new ArgumentNullException(nameof(ex.InnerException)); ex = ex.InnerException; } } /// /// Unmask the URL if it is masked /// /// The database connection /// The masked URL /// The backup ID /// The prefix, if this is a remote source /// The unmasked URL private static string UnmaskUrl(Connection connection, string maskedurl, string? backupId, long connectionStringId, string? prefix) { var backup = !string.IsNullOrWhiteSpace(backupId) ? connection.GetBackup(backupId) : null; var previousUrl = backup?.TargetURL; if (string.IsNullOrWhiteSpace(previousUrl) && connectionStringId > 0) previousUrl = connection.GetConnectionString(connectionStringId)?.BaseUrl; if (!string.IsNullOrWhiteSpace(prefix) && backup?.Sources != null) { previousUrl = backup.Sources?.FirstOrDefault(s => s.StartsWith(prefix + '|', StringComparison.Ordinal)); if (!string.IsNullOrWhiteSpace(previousUrl)) previousUrl = previousUrl.Split('|', 2).LastOrDefault(); } var unmasked = string.IsNullOrWhiteSpace(previousUrl) ? maskedurl : QuerystringMasking.Unmask(maskedurl, previousUrl); if (Connection.UrlContainsPasswordPlaceholder(unmasked)) throw new ArgumentException("Unmasked URL contains password placeholder"); return unmasked; } }