using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Duplicati.Server.Database; namespace Duplicati.Server; /// /// Helper class for starting the webserver /// public static class WebServerLoader { /// /// The tag used for logging /// private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(typeof(WebServerLoader)); /// /// Option for changing the webroot folder /// public const string OPTION_WEBROOT = "webservice-webroot"; /// /// Option for changing the webservice listen port /// public const string OPTION_PORT = "webservice-port"; /// /// Option for changing the webservice listen interface /// public const string OPTION_INTERFACE = "webservice-interface"; /// /// Option for setting the webservice password /// public const string OPTION_WEBSERVICE_PASSWORD = "webservice-password"; /// /// Option for resetting the JWT configuration /// public const string OPTION_WEBSERVICE_RESET_JWT_CONFIG = "webservice-reset-jwt-config"; /// /// Option for setting the webservice allowed hostnames /// public const string OPTION_WEBSERVICE_ALLOWEDHOSTNAMES = "webservice-allowedhostnames"; /// /// The default path to the web root /// public const string DEFAULT_OPTION_WEBROOT = "webroot"; /// /// The default listening port /// public const int DEFAULT_OPTION_PORT = 8200; /// /// Option for setting the webservice SSL certificate /// public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile"; /// /// Option for setting the webservice SSL certificate key /// public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword"; /// /// The default listening interface /// public const string DEFAULT_OPTION_INTERFACE = "loopback"; /// /// The parsed settings for the webserver /// /// The root folder with static files /// The listining port /// The listening interface /// The certificate, if any /// The servername to report /// The allowed hostnames public record ParsedWebserverSettings( string WebRoot, int Port, System.Net.IPAddress Interface, X509Certificate2? Certificate, string Servername, IEnumerable AllowedHostnames ); /// /// Sets up the webserver and starts it /// /// A set of options /// The method to start the server public static async Task TryRunServer(IReadOnlyDictionary options, Connection connection, Func> createServer) { var ports = Enumerable.Empty(); options.TryGetValue(OPTION_PORT, out var portstring); if (!string.IsNullOrEmpty(portstring)) ports = from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) where int.TryParse(n, out _) select int.Parse(n); if (ports == null || !ports.Any()) ports = [DEFAULT_OPTION_PORT]; options.TryGetValue(OPTION_INTERFACE, out var interfacestring); if (string.IsNullOrWhiteSpace(interfacestring)) interfacestring = connection.ApplicationSettings.ServerListenInterface; if (string.IsNullOrWhiteSpace(interfacestring)) interfacestring = DEFAULT_OPTION_INTERFACE; var listenInterface = System.Net.IPAddress.Loopback; interfacestring = interfacestring.Trim(); if (new[] { "*", "all", "any" }.Any(x => x.Equals(interfacestring, StringComparison.OrdinalIgnoreCase))) listenInterface = System.Net.IPAddress.Any; else if (interfacestring != "loopback") listenInterface = System.Net.IPAddress.Parse(interfacestring); options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out var certificateFile); options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out var certificateFilePassword); certificateFilePassword = certificateFilePassword?.Trim() ?? ""; X509Certificate2? cert = null; if (certificateFile == null) { try { cert = connection.ApplicationSettings.ServerSSLCertificate; } catch (Exception ex) { Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase); } } else if (certificateFile.Length == 0) { connection.ApplicationSettings.ServerSSLCertificate = null; } else { try { cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable); } catch (Exception ex) { throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex); } } var webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR; #if DEBUG //For debug we go "../../../../../.." to get out of "Executables/net8/Duplicati.GUI.TrayIcon/bin/debug/net8.0" string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", "..", "..", "..")); tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Duplicati", "Server"); if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) webroot = tmpwebroot; #endif webroot = System.IO.Path.Combine(webroot, "webroot"); if (options.ContainsKey(OPTION_WEBROOT)) { string userroot = options[OPTION_WEBROOT]; #if DEBUG //In debug mode we do not care where the path points #else //In release mode we check that the user supplied path is located // in the same folders as the running application, to avoid users // that inadvertently expose top level folders if (!string.IsNullOrWhiteSpace(userroot) && userroot.StartsWith(Duplicati.Library.Common.IO.Util.AppendDirSeparator(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison)) webroot = userroot; #endif } var certValid = cert != null && cert.HasPrivateKey; var settings = new ParsedWebserverSettings( webroot, -1, listenInterface, cert, string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version), options.GetValueOrDefault(OPTION_WEBSERVICE_ALLOWEDHOSTNAMES, "").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ); // If we are in hosted mode with no specified port, // then try different ports foreach (var p in ports) try { settings = settings with { Port = p }; var server = await createServer(settings); if (interfacestring != connection.ApplicationSettings.ServerListenInterface) connection.ApplicationSettings.ServerListenInterface = interfacestring; if (certValid && cert != connection.ApplicationSettings.ServerSSLCertificate) connection.ApplicationSettings.ServerSSLCertificate = cert; Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p)); return server; } catch (System.Net.Sockets.SocketException) { } throw new Exception(Strings.Server.ServerStartFailure(ports)); } }