From 63789f9132b4ac89f156b61158ccdbfd5fd7fbff Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Fri, 5 Dec 2025 13:22:13 +0100 Subject: [PATCH] Add password dialog Added a password dialog to get the server password if not provided on the commandline. This also allows overriding the server url. This is related to #6379 --- .../linux/context-menu-change-password.png | Bin 0 -> 4757 bytes .../icons/linux/context-menu-reconnect.png | Bin 0 -> 4850 bytes .../dark/context-menu-change-password.png | Bin 0 -> 7238 bytes .../macos/dark/context-menu-reconnect.png | Bin 0 -> 7753 bytes .../light/context-menu-change-password.png | Bin 0 -> 4757 bytes .../macos/light/context-menu-reconnect.png | Bin 0 -> 4850 bytes .../windows/context-menu-change-password.png | Bin 0 -> 8543 bytes .../icons/windows/context-menu-reconnect.png | Bin 0 -> 7811 bytes .../Duplicati.GUI.TrayIcon/AvaloniaRunner.cs | 46 ++ .../HttpServerConnection.cs | 55 +- .../Duplicati.GUI.TrayIcon/PasswordPrompt.cs | 234 ++++++ .../GUI/Duplicati.GUI.TrayIcon/Program.cs | 720 +++++++++--------- .../Duplicati.GUI.TrayIcon/TrayIconBase.cs | 32 +- 13 files changed, 716 insertions(+), 371 deletions(-) create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/linux/context-menu-change-password.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/linux/context-menu-reconnect.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/macos/dark/context-menu-change-password.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/macos/dark/context-menu-reconnect.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/macos/light/context-menu-change-password.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/macos/light/context-menu-reconnect.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/windows/context-menu-change-password.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/windows/context-menu-reconnect.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/PasswordPrompt.cs diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/linux/context-menu-change-password.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/linux/context-menu-change-password.png new file mode 100644 index 0000000000000000000000000000000000000000..48cd1e2dfd660601546cdb52e5f27da2fa9aa7f6 GIT binary patch literal 4757 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 FO#nTng3tf} literal 0 HcmV?d00001 diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/windows/context-menu-reconnect.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/windows/context-menu-reconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..3da009a324e2002a46abfa1d0b9891ef5fa89dc4 GIT binary patch literal 7811 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 + { + try + { + lifetime.Shutdown(); + } + catch { } + }); + return; + } + + // Password received, notify the callback + Program.Connection.UpdatePassword(password); + } + catch (Exception ex) + { + Log.WriteErrorMessage(LOGTAG, "PasswordPromptFailed", ex, "Failed to show password prompt"); + RunOnUIThreadInternal(() => + { + try + { + lifetime.Shutdown(); + } + catch { } + }); + } + } + protected override IMenuItem CreateMenuItem(string text, MenuIcons icon, Action callback, IList subitems) { return new AvaloniaMenuItem(this, text, icon, callback, subitems); @@ -348,6 +390,8 @@ namespace Duplicati.GUI.TrayIcon MenuIcons.Quit => AvaloniaRunner.LoadBitmap("context-menu-quit.png"), MenuIcons.Pause => AvaloniaRunner.LoadBitmap("context-menu-pause.png"), MenuIcons.Resume => AvaloniaRunner.LoadBitmap("context-menu-resume.png"), + MenuIcons.Reconnect => AvaloniaRunner.LoadBitmap("context-menu-reconnect.png"), + MenuIcons.ChangePassword => AvaloniaRunner.LoadBitmap("context-menu-change-password.png"), _ => null }; } @@ -490,6 +534,8 @@ namespace Duplicati.GUI.TrayIcon public override void OnFrameworkInitializationCompleted() { + RequestedThemeVariant = ThemeVariant.Default; + Styles.Add(new FluentTheme()); var icon = AvaloniaRunner.LoadIcon("normal.png"); diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs index 60c4f0e55..7ed4076dc 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs @@ -87,8 +87,9 @@ namespace Duplicati.GUI.TrayIcon private record BackgroundRequest(string Method, string Endpoint, string? Body, TimeSpan? Timeout = null); - private readonly string m_apiUri; - private readonly string m_baseUri; + private Uri m_serverUri; + private string m_apiUri; + private string m_baseUri; private string m_password; private string? m_accesstoken; private bool m_isTryingWithPassword; @@ -114,19 +115,45 @@ namespace Duplicati.GUI.TrayIcon private Task m_pollThread; private readonly CancellationTokenSource m_stopToken = new CancellationTokenSource(); - private readonly Dictionary m_options; private readonly Program.PasswordSource m_passwordSource; + private readonly TaskCompletionSource m_passwordUpdatedTcs = new TaskCompletionSource(); public IServerStatus Status { get { return m_status; } } + public void UpdatePassword(string newpassword) + { + m_password = newpassword; + m_accesstoken = null; + m_passwordUpdatedTcs.TrySetResult(newpassword); + } + + public void UpdateServerUri(Uri newServer) + { + if (newServer == null) + throw new ArgumentNullException(nameof(newServer)); + + m_serverUri = newServer; + m_baseUri = Util.AppendDirSeparator(m_serverUri.ToString(), "/"); + m_apiUri = m_baseUri + "api/v1"; + + // Reset state to ensure clean reconnection to the new server + m_lastEventId = 0; + m_lastDataUpdateId = -1; + m_lastNotificationId = -1; + } + private readonly IChannel m_workQueue = Channel.Create(name: "TrayIconRequestQueue"); private readonly CancellationToken _applicationExitEvent; + public Program.PasswordSource PasswordSource => m_passwordSource; + + public Uri ServerUri => m_serverUri; + public HttpServerConnection(IApplicationSettings? applicationSettings, System.Uri server, string password, Program.PasswordSource passwordSource, bool disableTrayIconLogin, string acceptedHostCertificate, Dictionary options) @@ -134,7 +161,8 @@ namespace Duplicati.GUI.TrayIcon _applicationExitEvent = applicationSettings?.ApplicationExit ?? CancellationToken.None; - m_baseUri = Util.AppendDirSeparator(server.ToString(), "/"); + m_serverUri = server; + m_baseUri = Util.AppendDirSeparator(m_serverUri.ToString(), "/"); m_apiUri = m_baseUri + "api/v1"; @@ -143,7 +171,6 @@ namespace Duplicati.GUI.TrayIcon m_firstNotificationTime = DateTime.Now; m_password = password; - m_options = options; m_passwordSource = passwordSource; var acceptedCertificates = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -208,8 +235,11 @@ namespace Duplicati.GUI.TrayIcon }); } - public Task UpdateStatus() - => UpdateStatusAsync(false); + public async Task UpdateStatus() + { + await PasswordAvailableIfNeeded().ConfigureAwait(false); + await UpdateStatusAsync(false).ConfigureAwait(false); + } private async Task UpdateStatusAsync(bool longpoll) { @@ -258,6 +288,15 @@ namespace Duplicati.GUI.TrayIcon m_disableTrayIconLogin = Library.Utility.Utility.ParseBool(str, false); } + private async Task PasswordAvailableIfNeeded() + { + if (string.IsNullOrWhiteSpace(m_password) && m_passwordSource == Program.PasswordSource.SuppliedPassword) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(m_stopToken.Token, _applicationExitEvent); + await m_passwordUpdatedTcs.Task.WaitAsync(cts.Token).ConfigureAwait(false); + } + } + private async Task LongPollRunner() { var started = DateTime.Now; @@ -276,6 +315,8 @@ namespace Duplicati.GUI.TrayIcon await Task.Delay(waitTime, cts.Token).ConfigureAwait(false); } + await PasswordAvailableIfNeeded().ConfigureAwait(false); + started = DateTime.Now; await UpdateStatusAsync(true).ConfigureAwait(false); errorCount = 0; diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/PasswordPrompt.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/PasswordPrompt.cs new file mode 100644 index 000000000..15f0c3ce5 --- /dev/null +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/PasswordPrompt.cs @@ -0,0 +1,234 @@ +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#nullable enable + +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Threading; + +namespace Duplicati.GUI.TrayIcon; + +internal static class PasswordPrompt +{ + public static bool IsShowingDialog { get; private set; } = false; + /// + /// Shows a password prompt dialog within an already-running Avalonia application. + /// Must be called from the UI thread or will dispatch to it. + /// + /// Whether this is a change password prompt + /// A task that completes with the password, or null if cancelled + public static Task ShowPasswordDialogAsync(bool isChangePassword) + { + if (IsShowingDialog) + throw new InvalidOperationException("A password prompt dialog is already showing."); + + IsShowingDialog = true; + var tcs = new TaskCompletionSource(); + + if (Dispatcher.UIThread.CheckAccess()) + { + ShowDialogInternal(tcs, isChangePassword); + } + else + { + Dispatcher.UIThread.Post(() => ShowDialogInternal(tcs, isChangePassword)); + } + + return tcs.Task.ContinueWith(t => + { + IsShowingDialog = false; + return t.Result; + }); + } + + private static void ShowDialogInternal(TaskCompletionSource tcs, bool isChangePassword) + { + try + { + var window = new PasswordPromptWindow(isChangePassword); + window.PasswordSubmitted += (_, pwd) => + { + tcs.TrySetResult(pwd); + window.Close(); + }; + + window.Cancelled += (_, _) => + { + tcs.TrySetResult(null); + window.Close(); + }; + + window.Closed += (_, _) => + { + // Ensure the task completes even if window is closed another way + tcs.TrySetResult(null); + }; + + window.Show(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Failed to show password prompt window: {ex.Message}"); + tcs.TrySetResult(null); + } + } +} + +internal class PasswordPromptWindow : Window +{ + private readonly TextBox passwordBox; + private readonly TextBox hostUrlBox; + + public event EventHandler? PasswordSubmitted; + + public event EventHandler? Cancelled; + + public PasswordPromptWindow(bool isChangePassword) + { + Title = "Duplicati TrayIcon Connect"; + Width = 420; + Height = 210; + CanResize = false; + WindowStartupLocation = WindowStartupLocation.CenterScreen; + + var description = new TextBlock + { + Text = isChangePassword + ? "Enter the new Duplicati server password:" + : "Enter the Duplicati server password to connect:", + Margin = new Thickness(0, 0, 0, 10), + TextWrapping = TextWrapping.Wrap + }; + + passwordBox = new TextBox + { + Watermark = "Server password", + PasswordChar = '•', + HorizontalAlignment = HorizontalAlignment.Stretch, + Margin = new Thickness(0, 0, 0, 10) + }; + + var hostUrlLabel = new TextBlock + { + Text = "Duplicati server URL:", + Margin = new Thickness(0, 5, 0, 0), + TextWrapping = TextWrapping.Wrap + }; + + hostUrlBox = new TextBox + { + Text = Program.Connection?.ServerUri?.ToString() ?? string.Empty, + Watermark = "http://localhost:8200", + HorizontalAlignment = HorizontalAlignment.Stretch, + Margin = new Thickness(0, 0, 0, 10) + }; + + var okButton = new Button + { + Content = "Connect", + IsDefault = true, + Width = 100 + }; + okButton.Click += (_, _) => SubmitPassword(); + + var cancelButton = new Button + { + Content = "Cancel", + IsCancel = true, + Width = 100 + }; + cancelButton.Click += (_, _) => Cancel(); + + var buttonPanel = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Spacing = 10 + }; + buttonPanel.Children.Add(cancelButton); + buttonPanel.Children.Add(okButton); + + var contentPanel = new Grid + { + RowDefinitions = new RowDefinitions("*,Auto") + }; + + var inputPanel = new StackPanel + { + Children = + { + description, + passwordBox, + hostUrlLabel, + hostUrlBox + } + }; + + Grid.SetRow(inputPanel, 0); + Grid.SetRow(buttonPanel, 1); + + contentPanel.Children.Add(inputPanel); + contentPanel.Children.Add(buttonPanel); + + Content = new Border + { + Padding = new Thickness(20), + Child = contentPanel + }; + + Opened += (_, _) => passwordBox.Focus(); + } + + private void SubmitPassword() + { + var password = passwordBox.Text; + var hostUrlText = hostUrlBox.Text?.Trim(); + + if (!string.IsNullOrWhiteSpace(hostUrlText) && Program.Connection != null) + { + try + { + var uri = new Uri(hostUrlText, UriKind.Absolute); + + if (!uri.Equals(Program.Connection.ServerUri)) + { + Program.Connection.UpdateServerUri(uri); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Invalid host URL '{hostUrlText}': {ex.Message}"); + } + } + + PasswordSubmitted?.Invoke(this, password); + } + + private void Cancel() + { + Cancelled?.Invoke(this, EventArgs.Empty); + } +} diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs index 277219b8e..399efa94f 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs @@ -1,362 +1,358 @@ -// Copyright (C) 2025, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using CoCoL; -using Duplicati.CommandLine; -using Duplicati.Library.AutoUpdater; -using Duplicati.Library.Interface; -using Duplicati.Library.Utility; -using Duplicati.Server; -using Duplicati.Server.Database; -using Duplicati.WebserverCore.Services; -using Microsoft.Extensions.DependencyInjection; -using Uri = System.Uri; - -namespace Duplicati.GUI.TrayIcon -{ - public static class Program - { - /// - /// The source of the password - /// - public enum PasswordSource - { - /// - /// No password, using token information from the database - /// - Database, - /// - /// No password supplied, using the hosted server - /// - HostedServer, - /// - /// The password was supplied on the commandline - /// - SuppliedPassword - } - - public static HttpServerConnection Connection; - - private const string HOSTURL_OPTION = "hosturl"; - private const string NOHOSTEDSERVER_OPTION = "no-hosted-server"; - private const string READCONFIGFROMDB_OPTION = "read-config-from-db"; - private const string ACCEPTED_SSL_CERTIFICATE = "host-cert-hash"; - - private const string DETACHED_PROCESS = "detached-process"; - private const string BROWSER_COMMAND_OPTION = "browser-command"; - private static readonly IReadOnlySet DETATCHED_WEBERVER_OPTIONS = new HashSet(StringComparer.OrdinalIgnoreCase) - { - WebServerLoader.OPTION_WEBSERVICE_PASSWORD, - WebServerLoader.OPTION_PORT, - DataFolderManager.SERVER_DATAFOLDER_OPTION, - DataFolderManager.PORTABLE_MODE_OPTION - }; - - - private static string DEFAULT_HOSTURL => $"http://{Utility.IpVersionCompatibleLoopback}:8200"; - - private static string _browser_command = null; - private static bool disableTrayIconLogin = false; - private static bool openui = false; - private static Uri serverURL = new(DEFAULT_HOSTURL); - public static string BrowserCommand { get { return _browser_command; } } - public static Server.Database.Connection databaseConnection = null; - - /// - /// The main entry point for the application. - /// - [STAThread] - public static int Main(string[] _args) - { - PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, PackageHelper.NamedExecutable.TrayIcon); - var args = new List(_args); - var options = CommandLineParser.ExtractOptions(args); - - if (OperatingSystem.IsWindows() && !Utility.ParseBoolOption(options, DETACHED_PROCESS)) - Win32.AttachConsole(Win32.ATTACH_PARENT_PROCESS); - - if (HelpOptionExtensions.IsArgumentAnyHelpString(_args)) - { - Console.WriteLine("Supported commandline arguments:"); - Console.WriteLine(); - - foreach (ICommandLineArgument arg in AllSupportedCommands) - Console.WriteLine("--{0}: {1}", arg.Name, arg.LongDescription); - - Console.WriteLine("Additionally, these server options are also supported:"); - Console.WriteLine(); - - foreach (ICommandLineArgument arg in WebserverSupportedCommands) - Console.WriteLine("--{0}: {1}", arg.Name, arg.LongDescription); - - return 0; - } - - // Apply secret provider, if any, so password etc can be retrieved - Library.Main.SecretProviderHelper.ApplySecretProviderAsync([], [], options, TempFolder.SystemTempPath, null, CancellationToken.None) - .Await(); - - options.TryGetValue(BROWSER_COMMAND_OPTION, out _browser_command); - - HostedInstanceKeeper hosted = null; - - string password = null; - var passwordSource = PasswordSource.SuppliedPassword; - var acceptedHostCertificate = options.GetValueOrDefault(ACCEPTED_SSL_CERTIFICATE, null); - var detached = Utility.ParseBoolOption(options, NOHOSTEDSERVER_OPTION); - - var supportedCommands = BasicSupportedCommands.AsEnumerable(); - if (detached) - { - supportedCommands = DetachSupportedCommands - .Concat(supportedCommands); - } - else - { - supportedCommands = supportedCommands.Concat(WebserverSupportedCommands); - } - - // Validate options, and log to console - using (var logger = new ConsoleOutput(Console.Out, options)) - CommandLineArgumentValidator.ValidateArguments(supportedCommands, options, Server.Program.KnownDuplicateOptions, new HashSet()); - - if (!detached) - { - try - { - // Tell the hosted server it was started by the TrayIcon - var applicationSettings = new ApplicationSettings - { - Origin = "Tray icon" - }; - passwordSource = PasswordSource.HostedServer; - // Ignore TrayIcon specific settings - foreach (var c in BasicSupportedCommands.Select(x => x.Name)) - Server.Program.ValidationIgnoredOptions.Add(c); - hosted = new HostedInstanceKeeper(applicationSettings, _args); - } - catch (Exception ex) - { - if (ex.InnerException != null && ex.InnerException is Server.SingleInstance.MultipleInstanceException) - { - Console.WriteLine(Server.Strings.Program.AnotherInstanceDetected); - return 1; - } - else - throw; - } - - // We have a hosted server, if this is the first run, - // we should open the main page - var connection = Server.Program.DuplicatiWebserver.Provider.GetRequiredService(); - openui = connection.ApplicationSettings.IsFirstRun || connection.ApplicationSettings.ServerPortChanged; - - var scheme = connection.ApplicationSettings.UseHTTPS ? "https" : "http"; - serverURL = new UriBuilder(serverURL) - { - Port = Server.Program.DuplicatiWebserver.Port, - Scheme = scheme - }.Uri; - - if (connection.ApplicationSettings.UseHTTPS && string.IsNullOrWhiteSpace(acceptedHostCertificate)) - acceptedHostCertificate = connection.ApplicationSettings.ServerSSLCertificate?.FirstOrDefault(x => x.HasPrivateKey)?.GetCertHashString(); - - } - else if (Utility.ParseBoolOption(options, READCONFIGFROMDB_OPTION)) - { - if (File.Exists(Path.Combine(DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ReadWritePermissionSet), DataFolderManager.SERVER_DATABASE_FILENAME))) - { - passwordSource = PasswordSource.Database; - databaseConnection = Server.Program.GetDatabaseConnection(new ApplicationSettings(), options, true, false); - - if (databaseConnection != null) - { - disableTrayIconLogin = databaseConnection.ApplicationSettings.DisableTrayIconLogin; - if (databaseConnection.ApplicationSettings.UseHTTPS && string.IsNullOrWhiteSpace(acceptedHostCertificate)) - acceptedHostCertificate = databaseConnection.ApplicationSettings.ServerSSLCertificate?.FirstOrDefault(x => x.HasPrivateKey)?.GetCertHashString(); - - var scheme = databaseConnection.ApplicationSettings.UseHTTPS ? "https" : "http"; - serverURL = new UriBuilder(serverURL) - { - Port = databaseConnection.ApplicationSettings.LastWebserverPort == -1 ? serverURL.Port : databaseConnection.ApplicationSettings.LastWebserverPort, - Scheme = scheme - }.Uri; - } - } - } - - // Legacy undocumented way to provide the password - if (options.TryGetValue("webserver-password", out var pwd)) - password = pwd; - if (options.TryGetValue(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, out pwd)) - password = pwd; - - // Let the user specify the port, if they are not providing a hosturl - if (!options.ContainsKey(HOSTURL_OPTION) && options.TryGetValue(WebServerLoader.OPTION_PORT, out var portString) && int.TryParse(portString, out var port)) - serverURL = new UriBuilder(serverURL) { Port = port }.Uri; - - if (options.TryGetValue(HOSTURL_OPTION, out var url)) - serverURL = new Uri(url); - - if (string.IsNullOrWhiteSpace(password) && passwordSource == PasswordSource.SuppliedPassword) - { - Console.WriteLine($@" -When running the TrayIcon without a hosted server, you must provide the server password via the option --{WebServerLoader.OPTION_WEBSERVICE_PASSWORD}=. -If the TrayIcon instance has read access to the server database, you can also or use the option --{READCONFIGFROMDB_OPTION}, possibly with --server-datafolder=. - -No password provided, unable to connect to server, exiting"); - return 1; - } - - StartTray(_args, options, hosted, passwordSource, password, acceptedHostCertificate); - - return 0; - } - - private static void StartTray(string[] _args, Dictionary options, HostedInstanceKeeper hosted, PasswordSource passwordSource, string password, string acceptedHostCertificate) - { - using (hosted) - { - var reSpawn = 0; - - do - { - if (reSpawn > 0) - Thread.Sleep(1000); - - try - { - using (Connection = new HttpServerConnection(hosted?.applicationSettings, serverURL, password, passwordSource, disableTrayIconLogin, acceptedHostCertificate, options)) - { - // Make sure we have the latest status, but don't care if it fails - Connection.UpdateStatus().FireAndForget(); - - using (var tk = RunTrayIcon()) - { - if (Server.Program.ApplicationInstance != null) - Server.Program.ApplicationInstance.SecondInstanceDetected += - x => tk.ShowStatusWindow(); - - // TODO: If we change to hosted browser this should be a callback - if (openui) - { - Connection.GetStatusWindowURLAsync().ContinueWith(t => - { - if (t.IsFaulted) - { - Console.WriteLine("Failed to get status window URL: " + t.Exception.Message); - tk.NotifyUser("Failed to get status window URL", t.Exception.Message, NotificationType.Error); - return; - } - - tk.ShowUrlInWindow(t.Result); - var connection = Server.Program.DuplicatiWebserver.Provider.GetRequiredService(); - connection.ApplicationSettings.IsFirstRun = false; - connection.ApplicationSettings.ServerPortChanged = false; - - }); - } - - // If the server shuts down, shut down the tray-icon as well - Action shutdownEvent = () => - { - // Make sure we do not start again after - // a controlled exit - reSpawn = 100; - tk.InvokeExit(); - }; - - Connection.ConnectionClosed = shutdownEvent; - if (hosted != null) - hosted.InstanceShutdown = shutdownEvent; - - tk.Init(_args); - - // If the tray-icon quits, stop the server - reSpawn = 100; - - // Make sure that the server shutdown does not access the tray-icon, - // as it would be disposed by now - if (hosted != null) - hosted.InstanceShutdown = null; - Connection.ConnectionClosed = null; - - } - } - - } - catch (HttpRequestException ex) - { - System.Diagnostics.Trace.WriteLine("Request error: " + ex); - Console.WriteLine("Request error: " + ex); - - reSpawn++; - } - catch (Exception ex) - { - System.Diagnostics.Trace.WriteLine("Unexpected error: " + ex); - Console.WriteLine("Unexpected error: " + ex); - return; - } - } while (reSpawn < 3); - } - } - - private static TrayIconBase RunTrayIcon() - => new AvaloniaRunner(); - - public static ICommandLineArgument[] DetachSupportedCommands => - [ - new CommandLineArgument(NOHOSTEDSERVER_OPTION, CommandLineArgument.ArgumentType.String, "Disables local server", "Set this option to not spawn a local service, use if the TrayIcon should connect to a running service"), - new CommandLineArgument(READCONFIGFROMDB_OPTION, CommandLineArgument.ArgumentType.String, "Read server connection info from DB", $"Set this option to read server connection info for running service from its database (only together with {NOHOSTEDSERVER_OPTION})"), - .. WebserverSupportedCommands.Where(x => DETATCHED_WEBERVER_OPTIONS.Contains(x.Name)) - ]; - - public static ICommandLineArgument[] BasicSupportedCommands => - [ - new CommandLineArgument(HOSTURL_OPTION, CommandLineArgument.ArgumentType.String, "Selects the url to connect to", "Supply the url that the TrayIcon will connect to and show status for", DEFAULT_HOSTURL), - new CommandLineArgument(BROWSER_COMMAND_OPTION, CommandLineArgument.ArgumentType.String, "Sets the browser command", "Set this option to override the default browser detection"), - new CommandLineArgument(ACCEPTED_SSL_CERTIFICATE, CommandLineArgument.ArgumentType.String, "Accepts a specific SSL certificate", "Set this option to accept a specific SSL certificate, the value should be the hash of the certificate in hexadecimal format. Use * to accept any certificate (dangerous)"), - .. WindowsSupportedCommands - ]; - - private static ICommandLineArgument[] WindowsSupportedCommands => - OperatingSystem.IsWindows() - ? [new CommandLineArgument(DETACHED_PROCESS, CommandLineArgument.ArgumentType.String, "Runs the tray-icon detached", "This option runs the tray-icon in detached mode, meaning that the process will exit immediately and not send output to the console of the caller")] - : []; - - public static ICommandLineArgument[] WebserverSupportedCommands - => Server.Program.SupportedCommands; - - public static ICommandLineArgument[] AllSupportedCommands => - [ - .. DetachSupportedCommands, - .. BasicSupportedCommands - ]; - } -} +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using CoCoL; +using Duplicati.CommandLine; +using Duplicati.Library.AutoUpdater; +using Duplicati.Library.Interface; +using Duplicati.Library.Utility; +using Duplicati.Server; +using Duplicati.Server.Database; +using Duplicati.WebserverCore.Services; +using Microsoft.Extensions.DependencyInjection; +using Uri = System.Uri; + +namespace Duplicati.GUI.TrayIcon +{ + public static class Program + { + /// + /// The source of the password + /// + public enum PasswordSource + { + /// + /// No password, using token information from the database + /// + Database, + /// + /// No password supplied, using the hosted server + /// + HostedServer, + /// + /// The password was supplied on the commandline + /// + SuppliedPassword + } + + public static HttpServerConnection Connection; + + /// + /// Indicates that a password is needed and should be requested via GUI after Avalonia starts + /// + public static bool NeedsPasswordPrompt { get; private set; } + + private const string HOSTURL_OPTION = "hosturl"; + private const string NOHOSTEDSERVER_OPTION = "no-hosted-server"; + private const string READCONFIGFROMDB_OPTION = "read-config-from-db"; + private const string ACCEPTED_SSL_CERTIFICATE = "host-cert-hash"; + + private const string DETACHED_PROCESS = "detached-process"; + private const string BROWSER_COMMAND_OPTION = "browser-command"; + private static readonly IReadOnlySet DETATCHED_WEBERVER_OPTIONS = new HashSet(StringComparer.OrdinalIgnoreCase) + { + WebServerLoader.OPTION_WEBSERVICE_PASSWORD, + WebServerLoader.OPTION_PORT, + DataFolderManager.SERVER_DATAFOLDER_OPTION, + DataFolderManager.PORTABLE_MODE_OPTION + }; + + + private static string DEFAULT_HOSTURL => $"http://{Utility.IpVersionCompatibleLoopback}:8200"; + + private static string _browser_command = null; + private static bool disableTrayIconLogin = false; + private static bool openui = false; + private static Uri serverURL = new(DEFAULT_HOSTURL); + public static string BrowserCommand { get { return _browser_command; } } + public static Server.Database.Connection databaseConnection = null; + + /// + /// The main entry point for the application. + /// + [STAThread] + public static int Main(string[] _args) + { + PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, PackageHelper.NamedExecutable.TrayIcon); + var args = new List(_args); + var options = CommandLineParser.ExtractOptions(args); + + if (OperatingSystem.IsWindows() && !Utility.ParseBoolOption(options, DETACHED_PROCESS)) + Win32.AttachConsole(Win32.ATTACH_PARENT_PROCESS); + + if (HelpOptionExtensions.IsArgumentAnyHelpString(_args)) + { + Console.WriteLine("Supported commandline arguments:"); + Console.WriteLine(); + + foreach (ICommandLineArgument arg in AllSupportedCommands) + Console.WriteLine("--{0}: {1}", arg.Name, arg.LongDescription); + + Console.WriteLine("Additionally, these server options are also supported:"); + Console.WriteLine(); + + foreach (ICommandLineArgument arg in WebserverSupportedCommands) + Console.WriteLine("--{0}: {1}", arg.Name, arg.LongDescription); + + return 0; + } + + // Apply secret provider, if any, so password etc can be retrieved + Library.Main.SecretProviderHelper.ApplySecretProviderAsync([], [], options, TempFolder.SystemTempPath, null, CancellationToken.None) + .Await(); + + options.TryGetValue(BROWSER_COMMAND_OPTION, out _browser_command); + + HostedInstanceKeeper hosted = null; + + string password = null; + var passwordSource = PasswordSource.SuppliedPassword; + var acceptedHostCertificate = options.GetValueOrDefault(ACCEPTED_SSL_CERTIFICATE, null); + var detached = Utility.ParseBoolOption(options, NOHOSTEDSERVER_OPTION); + + var supportedCommands = BasicSupportedCommands.AsEnumerable(); + if (detached) + { + supportedCommands = DetachSupportedCommands + .Concat(supportedCommands); + } + else + { + supportedCommands = supportedCommands.Concat(WebserverSupportedCommands); + } + + // Validate options, and log to console + using (var logger = new ConsoleOutput(Console.Out, options)) + CommandLineArgumentValidator.ValidateArguments(supportedCommands, options, Server.Program.KnownDuplicateOptions, new HashSet()); + + if (!detached) + { + try + { + // Tell the hosted server it was started by the TrayIcon + var applicationSettings = new ApplicationSettings + { + Origin = "Tray icon" + }; + passwordSource = PasswordSource.HostedServer; + // Ignore TrayIcon specific settings + foreach (var c in BasicSupportedCommands.Select(x => x.Name)) + Server.Program.ValidationIgnoredOptions.Add(c); + hosted = new HostedInstanceKeeper(applicationSettings, _args); + } + catch (Exception ex) + { + if (ex.InnerException != null && ex.InnerException is Server.SingleInstance.MultipleInstanceException) + { + Console.WriteLine(Server.Strings.Program.AnotherInstanceDetected); + return 1; + } + else + throw; + } + + // We have a hosted server, if this is the first run, + // we should open the main page + var connection = Server.Program.DuplicatiWebserver.Provider.GetRequiredService(); + openui = connection.ApplicationSettings.IsFirstRun || connection.ApplicationSettings.ServerPortChanged; + + var scheme = connection.ApplicationSettings.UseHTTPS ? "https" : "http"; + serverURL = new UriBuilder(serverURL) + { + Port = Server.Program.DuplicatiWebserver.Port, + Scheme = scheme + }.Uri; + + if (connection.ApplicationSettings.UseHTTPS && string.IsNullOrWhiteSpace(acceptedHostCertificate)) + acceptedHostCertificate = connection.ApplicationSettings.ServerSSLCertificate?.FirstOrDefault(x => x.HasPrivateKey)?.GetCertHashString(); + + } + else if (Utility.ParseBoolOption(options, READCONFIGFROMDB_OPTION)) + { + if (File.Exists(Path.Combine(DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ReadWritePermissionSet), DataFolderManager.SERVER_DATABASE_FILENAME))) + { + passwordSource = PasswordSource.Database; + databaseConnection = Server.Program.GetDatabaseConnection(new ApplicationSettings(), options, true, false); + + if (databaseConnection != null) + { + disableTrayIconLogin = databaseConnection.ApplicationSettings.DisableTrayIconLogin; + if (databaseConnection.ApplicationSettings.UseHTTPS && string.IsNullOrWhiteSpace(acceptedHostCertificate)) + acceptedHostCertificate = databaseConnection.ApplicationSettings.ServerSSLCertificate?.FirstOrDefault(x => x.HasPrivateKey)?.GetCertHashString(); + + var scheme = databaseConnection.ApplicationSettings.UseHTTPS ? "https" : "http"; + serverURL = new UriBuilder(serverURL) + { + Port = databaseConnection.ApplicationSettings.LastWebserverPort == -1 ? serverURL.Port : databaseConnection.ApplicationSettings.LastWebserverPort, + Scheme = scheme + }.Uri; + } + } + } + + // Legacy undocumented way to provide the password + if (options.TryGetValue("webserver-password", out var pwd)) + password = pwd; + if (options.TryGetValue(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, out pwd)) + password = pwd; + + // Let the user specify the port, if they are not providing a hosturl + if (!options.ContainsKey(HOSTURL_OPTION) && options.TryGetValue(WebServerLoader.OPTION_PORT, out var portString) && int.TryParse(portString, out var port)) + serverURL = new UriBuilder(serverURL) { Port = port }.Uri; + + if (options.TryGetValue(HOSTURL_OPTION, out var url)) + serverURL = new Uri(url); + + if (string.IsNullOrWhiteSpace(password) && passwordSource == PasswordSource.SuppliedPassword) + NeedsPasswordPrompt = true; + + StartTray(_args, options, hosted, passwordSource, password, acceptedHostCertificate); + + return 0; + } + + private static void StartTray(string[] _args, Dictionary options, HostedInstanceKeeper hosted, PasswordSource passwordSource, string password, string acceptedHostCertificate) + { + using (hosted) + { + var reSpawn = 0; + + do + { + if (reSpawn > 0) + Thread.Sleep(1000); + + try + { + using (Connection = new HttpServerConnection(hosted?.applicationSettings, serverURL, password, passwordSource, disableTrayIconLogin, acceptedHostCertificate, options)) + { + // Make sure we have the latest status, but don't care if it fails + Connection.UpdateStatus().FireAndForget(); + using (var tk = RunTrayIcon()) + { + if (Server.Program.ApplicationInstance != null) + Server.Program.ApplicationInstance.SecondInstanceDetected += + x => tk.ShowStatusWindow(); + + // TODO: If we change to hosted browser this should be a callback + if (openui) + { + Connection.GetStatusWindowURLAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + Console.WriteLine("Failed to get status window URL: " + t.Exception.Message); + tk.NotifyUser("Failed to get status window URL", t.Exception.Message, NotificationType.Error); + return; + } + + tk.ShowUrlInWindow(t.Result); + var connection = Server.Program.DuplicatiWebserver.Provider.GetRequiredService(); + connection.ApplicationSettings.IsFirstRun = false; + connection.ApplicationSettings.ServerPortChanged = false; + + }); + } + + // If the server shuts down, shut down the tray-icon as well + Action shutdownEvent = () => + { + // Make sure we do not start again after + // a controlled exit + reSpawn = 100; + tk.InvokeExit(); + }; + + Connection.ConnectionClosed = shutdownEvent; + if (hosted != null) + hosted.InstanceShutdown = shutdownEvent; + + tk.Init(_args); + + // If the tray-icon quits, stop the server + reSpawn = 100; + + // Make sure that the server shutdown does not access the tray-icon, + // as it would be disposed by now + if (hosted != null) + hosted.InstanceShutdown = null; + Connection.ConnectionClosed = null; + + } + } + + } + catch (HttpRequestException ex) + { + System.Diagnostics.Trace.WriteLine("Request error: " + ex); + Console.WriteLine("Request error: " + ex); + + reSpawn++; + } + catch (Exception ex) + { + System.Diagnostics.Trace.WriteLine("Unexpected error: " + ex); + Console.WriteLine("Unexpected error: " + ex); + return; + } + } while (reSpawn < 3); + } + } + + private static TrayIconBase RunTrayIcon() + => new AvaloniaRunner(); + + public static ICommandLineArgument[] DetachSupportedCommands => + [ + new CommandLineArgument(NOHOSTEDSERVER_OPTION, CommandLineArgument.ArgumentType.String, "Disables local server", "Set this option to not spawn a local service, use if the TrayIcon should connect to a running service"), + new CommandLineArgument(READCONFIGFROMDB_OPTION, CommandLineArgument.ArgumentType.String, "Read server connection info from DB", $"Set this option to read server connection info for running service from its database (only together with {NOHOSTEDSERVER_OPTION})"), + .. WebserverSupportedCommands.Where(x => DETATCHED_WEBERVER_OPTIONS.Contains(x.Name)) + ]; + + public static ICommandLineArgument[] BasicSupportedCommands => + [ + new CommandLineArgument(HOSTURL_OPTION, CommandLineArgument.ArgumentType.String, "Selects the url to connect to", "Supply the url that the TrayIcon will connect to and show status for", DEFAULT_HOSTURL), + new CommandLineArgument(BROWSER_COMMAND_OPTION, CommandLineArgument.ArgumentType.String, "Sets the browser command", "Set this option to override the default browser detection"), + new CommandLineArgument(ACCEPTED_SSL_CERTIFICATE, CommandLineArgument.ArgumentType.String, "Accepts a specific SSL certificate", "Set this option to accept a specific SSL certificate, the value should be the hash of the certificate in hexadecimal format. Use * to accept any certificate (dangerous)"), + .. WindowsSupportedCommands + ]; + + private static ICommandLineArgument[] WindowsSupportedCommands => + OperatingSystem.IsWindows() + ? [new CommandLineArgument(DETACHED_PROCESS, CommandLineArgument.ArgumentType.String, "Runs the tray-icon detached", "This option runs the tray-icon in detached mode, meaning that the process will exit immediately and not send output to the console of the caller")] + : []; + + public static ICommandLineArgument[] WebserverSupportedCommands + => Server.Program.SupportedCommands; + + public static ICommandLineArgument[] AllSupportedCommands => + [ + .. DetachSupportedCommands, + .. BasicSupportedCommands + ]; + } +} diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs index 206b1a3fe..5110f2a63 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs @@ -24,6 +24,7 @@ using Duplicati.Server.Serialization; using Duplicati.Server.Serialization.Interface; using System.Collections.Generic; using System.Threading.Tasks; +using CoCoL; namespace Duplicati.GUI.TrayIcon { @@ -34,6 +35,8 @@ namespace Duplicati.GUI.TrayIcon Quit, Pause, Resume, + Reconnect, + ChangePassword } public enum TrayIcons @@ -73,6 +76,7 @@ namespace Duplicati.GUI.TrayIcon public abstract class TrayIconBase : IDisposable { protected IMenuItem m_reconnectMenu; + protected IMenuItem m_changePasswordMenu; protected IMenuItem m_openMenu; protected IMenuItem m_pauseMenu; protected IMenuItem m_quitMenu; @@ -161,18 +165,41 @@ namespace Duplicati.GUI.TrayIcon protected IEnumerable BuildMenu() { - m_reconnectMenu = CreateMenuItem("Reconnect", MenuIcons.Resume, Reconnect, null); + m_reconnectMenu = CreateMenuItem("Reconnect", MenuIcons.Reconnect, Reconnect, null); m_reconnectMenu.SetHidden(true); + m_changePasswordMenu = CreateMenuItem("Change Password", MenuIcons.ChangePassword, OnChangePasswordClicked, null); + m_changePasswordMenu.SetHidden(true); m_openMenu = CreateMenuItem("Open", MenuIcons.Status, OnStatusClicked, null); m_openMenu.SetDefault(true); return [ m_reconnectMenu, + m_changePasswordMenu, m_openMenu, m_pauseMenu = CreateMenuItem("Pause", MenuIcons.Pause, OnPauseClicked, null ), m_quitMenu = CreateMenuItem("Quit", MenuIcons.Quit, OnQuitClicked, null), ]; } + private void OnChangePasswordClicked() + { + PasswordPrompt.ShowPasswordDialogAsync(isChangePassword: true) + .ContinueWith(t => + { + OnStatusUpdated(Program.Connection.Status); + if (t.IsFaulted) + return; + + if (string.IsNullOrWhiteSpace(t.Result)) + return; + + Program.Connection.UpdatePassword(t.Result); + }) + .FireAndForget(); + + OnStatusUpdated(Program.Connection.Status); + + } + private void Reconnect() { Program.Connection.UpdateStatus().ContinueWith(t => @@ -269,7 +296,8 @@ namespace Duplicati.GUI.TrayIcon m_openMenu.SetHidden(status.SuggestedStatusIcon == SuggestedStatusIcon.Disconnected); m_pauseMenu.SetHidden(status.SuggestedStatusIcon == SuggestedStatusIcon.Disconnected); - m_reconnectMenu.SetHidden(status.SuggestedStatusIcon != SuggestedStatusIcon.Disconnected); + m_reconnectMenu.SetHidden(status.SuggestedStatusIcon != SuggestedStatusIcon.Disconnected || PasswordPrompt.IsShowingDialog); + m_changePasswordMenu.SetHidden(status.SuggestedStatusIcon != SuggestedStatusIcon.Disconnected || Program.Connection?.PasswordSource != Program.PasswordSource.SuppliedPassword || PasswordPrompt.IsShowingDialog); }); #region IDisposable implementation