using Duplicati.Server.Database; using Duplicati.WebserverCore.Abstractions; using Duplicati.WebserverCore.Exceptions; using Microsoft.AspNetCore.Mvc; namespace Duplicati.WebserverCore.Endpoints.V1; public class ServerSetting : IEndpointV1 { public static void Map(RouteGroupBuilder group) { group.MapGet("/serversettings", ([FromServices] Connection connection) => GetSettings(connection)).RequireAuthorization(); group.MapPatch("/serversettings", ([FromServices] Connection connection, [FromBody] Dictionary values) => UpdateSettings(connection, values)).RequireAuthorization(); group.MapGet("/serversetting/{key}", ([FromRoute] string key, [FromServices] Connection connection, [FromServices] ISettingsService settingsService) => GetSetting(key, connection, settingsService)).RequireAuthorization(); group.MapPut("/serversetting/{key}", ([FromRoute] string key, [FromBody] string value, [FromServices] Connection connection) => UpdateSetting(key, value, connection)).RequireAuthorization(); } // Remove sensitive information from the output private static readonly string[] GUARDED_OUTPUT = [ Server.Database.ServerSettings.CONST.JWT_CONFIG, Server.Database.ServerSettings.CONST.PBKDF_CONFIG, // Not used anymore, but not completely removed Server.Database.ServerSettings.CONST.SERVER_PASSPHRASE, Server.Database.ServerSettings.CONST.SERVER_PASSPHRASE_SALT, // Completely removed, but no need to expose "server-passphrase-trayicon-hash", "server-passphrase-trayicon-salt" ]; private static readonly string[] GUARDED_INPUT = [ Server.Database.ServerSettings.CONST.JWT_CONFIG, Server.Database.ServerSettings.CONST.PBKDF_CONFIG, Server.Database.ServerSettings.CONST.SERVER_PASSPHRASE, Server.Database.ServerSettings.CONST.SERVER_PASSPHRASE_SALT, Server.Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATE, "ServerSSLCertificate", "server-passphrase-trayicon-hash", "server-passphrase-trayicon-salt" ]; private static Dictionary GetSettings(Connection connection) { // Join server settings and global settings var dict = connection.GetSettings(Connection.SERVER_SETTINGS_ID) .Where(x => !string.IsNullOrWhiteSpace(x.Name)) .Union( connection.Settings .Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name.StartsWith("--", StringComparison.Ordinal)) ) .DistinctBy(x => x.Name) .ToDictionary(x => x.Name, x => x.Value); // Patch cert to boolean dict.TryGetValue("server-ssl-certificate", out var sslcert); dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString(); foreach (var key in GUARDED_OUTPUT) dict.Remove(key); return dict; } private static void UpdateSettings(Connection connection, Dictionary? values) { if (values == null) throw new BadRequestException("No values provided"); var passphrase = values.GetValueOrDefault(Server.Database.ServerSettings.CONST.SERVER_PASSPHRASE)?.ToString(); foreach (var key in GUARDED_INPUT) values.Remove(key); // Split into server settings and global settings var serversettings = values.Where(x => !string.IsNullOrWhiteSpace(x.Key) && !x.Key.StartsWith("--", StringComparison.Ordinal)) .ToDictionary(x => x.Key, x => x.Value?.ToString()); var globalsettings = values.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal)); if (!string.IsNullOrWhiteSpace(passphrase)) connection.ApplicationSettings.SetWebserverPassword(passphrase); if (serversettings.Any()) connection.ApplicationSettings.UpdateSettings(serversettings, false); if (globalsettings.Any()) { // Update based on inputs var existing = connection.Settings.ToDictionary(x => x.Name, x => x); foreach (var g in globalsettings) if (g.Value == null) existing.Remove(g.Key); else { if (existing.ContainsKey(g.Key)) existing[g.Key].Value = g.Value?.ToString(); else existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value?.ToString() }; } connection.Settings = existing.Select(x => x.Value).ToArray(); } } private static string? GetSetting(string key, Connection connection, ISettingsService settingsService) { if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase)) return settingsService.GetSettings().HasSSLCertificate.ToString(); if (GUARDED_OUTPUT.Any(x => string.Equals(x, key, StringComparison.OrdinalIgnoreCase))) throw new NotFoundException("Key not found"); if (key.StartsWith("--", StringComparison.Ordinal)) { return connection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase))?.Value; } else { var prop = connection.ApplicationSettings.GetType().GetProperty(key); if (prop == null) throw new NotFoundException("Key not found"); return prop.GetValue(connection.ApplicationSettings)?.ToString(); } } private static void UpdateSetting(string key, string value, Connection connection) { if (GUARDED_INPUT.Any(x => string.Equals(x, key, StringComparison.OrdinalIgnoreCase))) throw new BadRequestException($"Cannot update {key} setting"); if (key.StartsWith("--", StringComparison.Ordinal)) { var settings = connection.Settings.ToList(); var prop = settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); if (prop == null) settings.Add(prop = new Server.Database.Setting() { Name = key, Value = value }); else prop.Value = value; connection.Settings = settings.ToArray(); } else { var prop = connection.ApplicationSettings.GetType().GetProperty(key); if (prop == null) throw new NotFoundException("Key not found"); var dict = new Dictionary { { key, value } }; connection.ApplicationSettings.UpdateSettings(dict, false); } } }