Merge pull request #6777 from duplicati/feature/assign-license-from-console
Improved license key support
This commit is contained in:
@@ -97,6 +97,10 @@ public sealed record ControlRequestMessage(string Command, Dictionary<string, st
|
||||
/// </summary>
|
||||
public const string ReportUrlKey = "reportingurl";
|
||||
/// <summary>
|
||||
/// The key that contains the client license key
|
||||
/// </summary>
|
||||
public const string ClientLicenseKeyKey = "clientlicensekey";
|
||||
/// <summary>
|
||||
/// The key that contains the dashboard URL
|
||||
/// </summary>
|
||||
public const string DashboardUrlKey = "dashboardurl";
|
||||
|
||||
@@ -79,7 +79,8 @@ namespace Duplicati.Server.Database
|
||||
ServerSettings.CONST.REMOTE_CONTROL_CONFIG,
|
||||
ServerSettings.CONST.SERVER_SSL_CERTIFICATE,
|
||||
ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD,
|
||||
ServerSettings.CONST.REMOTE_CONTROL_STORAGE_API_KEY
|
||||
ServerSettings.CONST.REMOTE_CONTROL_STORAGE_API_KEY,
|
||||
ServerSettings.CONST.CLIENT_LICENSE_KEY
|
||||
])
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ namespace Duplicati.Server.Database
|
||||
public const string REMOTE_CONTROL_STORAGE_API_ID = "remote-control-storage-api-id";
|
||||
public const string REMOTE_CONTROL_STORAGE_API_KEY = "remote-control-storage-api-key";
|
||||
public const string REMOTE_CONTROL_STORAGE_ENDPOINT_URL = "remote-control-storage-endpoint-url";
|
||||
public const string CLIENT_LICENSE_KEY = "client-license-key";
|
||||
public const string ENABLE_FOLDER_STATUS_SERVICE = "enable-folder-status-service";
|
||||
}
|
||||
|
||||
@@ -709,6 +710,12 @@ namespace Duplicati.Server.Database
|
||||
set => SetAndSaveSetting(CONST.REMOTE_CONTROL_STORAGE_ENDPOINT_URL, value);
|
||||
}
|
||||
|
||||
public string? ClientLicenseKey
|
||||
{
|
||||
get => settings[CONST.CLIENT_LICENSE_KEY];
|
||||
set => SetAndSaveSetting(CONST.CLIENT_LICENSE_KEY, value);
|
||||
}
|
||||
|
||||
public bool EnableFolderStatusService
|
||||
{
|
||||
get => Utility.ParseBool(settings[CONST.ENABLE_FOLDER_STATUS_SERVICE], false);
|
||||
|
||||
@@ -293,6 +293,9 @@ namespace Duplicati.Server
|
||||
if (!string.IsNullOrWhiteSpace(remoteControlUrl))
|
||||
RegisterForRemoteControl(DuplicatiWebserver.Provider.GetRequiredService<IRemoteControllerRegistration>(), DuplicatiWebserver.Provider.GetRequiredService<IRemoteController>(), remoteControlUrl, Library.Utility.Utility.ParseBoolOption(commandlineOptions, REGISTER_REMOTE_CONTROL_REREGISTER_OPTION), logMessageToConsole);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(connection.ApplicationSettings.ClientLicenseKey))
|
||||
Proprietary.LicenseChecker.LicenseHelper.SetRemoteClientLicenseKey(connection.ApplicationSettings.ClientLicenseKey);
|
||||
|
||||
if (applicationSettings.Origin == "Server" && connection.ApplicationSettings.AutogeneratedPassphrase)
|
||||
{
|
||||
var signinToken = DuplicatiWebserver.Provider.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("server-cli");
|
||||
|
||||
@@ -125,4 +125,32 @@ public class ServerSettings
|
||||
{
|
||||
get => applicationSettings.ServerSSLCertificate != null;
|
||||
}
|
||||
}
|
||||
|
||||
public class LicenseStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a license is configured.
|
||||
/// </summary>
|
||||
public required bool IsConfigured { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the license is currently valid.
|
||||
/// </summary>
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the license is in its grace period (expired but still functional).
|
||||
/// </summary>
|
||||
public required bool IsInGracePeriod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The license expiration date (null if no license configured).
|
||||
/// </summary>
|
||||
public required DateTimeOffset? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The license expiration date including grace period (null if no license configured).
|
||||
/// </summary>
|
||||
public required DateTimeOffset? ExpiresWithGraceAt { get; init; }
|
||||
}
|
||||
@@ -247,6 +247,78 @@ public sealed record SystemInfoDto
|
||||
/// </summary>
|
||||
public required IEnumerable<string> PowerModeProviders { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local license status (from file or environment only, not server-provided).
|
||||
/// </summary>
|
||||
public required LicenseStatusDto? LocalLicenseStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the remote license status (from server-provided license).
|
||||
/// </summary>
|
||||
public required LicenseStatusDto? RemoteLicenseStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Represents the license status information.
|
||||
/// </summary>
|
||||
public class LicenseStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a local license is configured (from file or environment).
|
||||
/// </summary>
|
||||
public required bool IsConfigured { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the local license is currently valid.
|
||||
/// </summary>
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the license is in its grace period (expired but still functional).
|
||||
/// </summary>
|
||||
public required bool IsInGracePeriod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The license expiration date (null if no license configured).
|
||||
/// </summary>
|
||||
public required DateTimeOffset? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The license expiration date including grace period (null if no license configured).
|
||||
/// </summary>
|
||||
public required DateTimeOffset? ExpiresWithGraceAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The enabled features from the license.
|
||||
/// </summary>
|
||||
public required Dictionary<string, string>? Features { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maps the license data to a DTO.
|
||||
/// </summary>
|
||||
/// <param name="data">The license data to map.</param>
|
||||
/// <returns>The mapped DTO.</returns>
|
||||
public static LicenseStatusDto Map(Proprietary.LicenseChecker.LicenseData? data)
|
||||
=> data != null
|
||||
? new LicenseStatusDto
|
||||
{
|
||||
IsConfigured = true,
|
||||
IsValid = data.IsValidNow,
|
||||
IsInGracePeriod = data.IsInGracePeriod,
|
||||
ExpiresAt = data.ValidTo,
|
||||
ExpiresWithGraceAt = data.ValidToWithGrace,
|
||||
Features = data.Features
|
||||
}
|
||||
: new LicenseStatusDto
|
||||
{
|
||||
IsConfigured = false,
|
||||
IsValid = false,
|
||||
IsInGracePeriod = false,
|
||||
ExpiresAt = null,
|
||||
ExpiresWithGraceAt = null,
|
||||
Features = null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a timezone.
|
||||
/// </summary>
|
||||
|
||||
@@ -98,6 +98,15 @@ public class RemoteControllerHandler(Connection connection, IHttpClientFactory h
|
||||
connection.ApplicationSettings.RemoteControlStorageApiKey = message.ControlRequestMessage.Parameters.GetValueOrDefault(ControlRequestMessage.StorageApiKeyKey);
|
||||
connection.ApplicationSettings.RemoteControlStorageEndpointUrl = message.ControlRequestMessage.Parameters.GetValueOrDefault(ControlRequestMessage.StorageEndpointUrlKey);
|
||||
|
||||
var newLicenseKey = message.ControlRequestMessage.Parameters.GetValueOrDefault(ControlRequestMessage.ClientLicenseKeyKey);
|
||||
var currentLicenseKey = connection.ApplicationSettings.ClientLicenseKey;
|
||||
|
||||
if (newLicenseKey != currentLicenseKey)
|
||||
{
|
||||
connection.ApplicationSettings.ClientLicenseKey = newLicenseKey;
|
||||
Duplicati.Proprietary.LicenseChecker.LicenseHelper.SetRemoteClientLicenseKey(newLicenseKey);
|
||||
}
|
||||
|
||||
var backupConfigs = message.ControlRequestMessage.Parameters
|
||||
.Where(kv => kv.Key.StartsWith(ControlRequestMessage.BackupConfigKeyPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(kv => kv.Key)
|
||||
|
||||
@@ -109,6 +109,8 @@ public class RemoteControllerService(Connection connection, IRemoteControllerHan
|
||||
connection.ApplicationSettings.RemoteControlStorageApiId = string.Empty;
|
||||
connection.ApplicationSettings.RemoteControlStorageApiKey = string.Empty;
|
||||
connection.ApplicationSettings.RemoteControlStorageEndpointUrl = string.Empty;
|
||||
connection.ApplicationSettings.ClientLicenseKey = string.Empty;
|
||||
Duplicati.Proprietary.LicenseChecker.LicenseHelper.SetRemoteClientLicenseKey(null);
|
||||
|
||||
// Remove remotely configured backups
|
||||
var remoteBackupIds = connection.Backups.Where(x => !string.IsNullOrWhiteSpace(x.ExternalID) && x.ExternalID.StartsWith(ControlRequestMessage.BackupConfigKeyPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -36,6 +36,7 @@ public class SettingsService(Connection connection) : ISettingsService
|
||||
Server.Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATE,
|
||||
Server.Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATEPASSWORD,
|
||||
Server.Database.ServerSettings.CONST.REMOTE_CONTROL_STORAGE_API_KEY,
|
||||
Server.Database.ServerSettings.CONST.CLIENT_LICENSE_KEY,
|
||||
// Not used anymore, but not completely removed
|
||||
Server.Database.ServerSettings.CONST.SERVER_PASSPHRASE,
|
||||
Server.Database.ServerSettings.CONST.SERVER_PASSPHRASE_SALT,
|
||||
@@ -60,6 +61,7 @@ public class SettingsService(Connection connection) : ISettingsService
|
||||
Server.Database.ServerSettings.CONST.REMOTE_CONTROL_STORAGE_ENDPOINT_URL,
|
||||
Server.Database.ServerSettings.CONST.REMOTE_CONTROL_STORAGE_API_KEY,
|
||||
Server.Database.ServerSettings.CONST.REMOTE_CONTROL_STORAGE_API_ID,
|
||||
Server.Database.ServerSettings.CONST.CLIENT_LICENSE_KEY,
|
||||
"ServerSSLCertificate",
|
||||
"server-passphrase-trayicon-hash",
|
||||
"server-passphrase-trayicon-salt"
|
||||
|
||||
@@ -397,6 +397,8 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec
|
||||
DefaultOAuthURL = AuthIdOptionsHelper.DUPLICATI_OAUTH_SERVICE,
|
||||
DefaultOAuthURLv2 = AuthIdOptionsHelper.DUPLICATI_OAUTH_SERVICE_NEW,
|
||||
PowerModeProviders = systeminfo.PowerModeProviders,
|
||||
LocalLicenseStatus = SystemInfoDto.LicenseStatusDto.Map(Proprietary.LicenseChecker.LicenseHelper.GetLocalLicenseData()),
|
||||
RemoteLicenseStatus = SystemInfoDto.LicenseStatusDto.Map(Proprietary.LicenseChecker.LicenseHelper.GetRemoteLicenseData())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,131 @@ namespace Duplicati.Proprietary.LicenseChecker;
|
||||
|
||||
public static class LicenseHelper
|
||||
{
|
||||
public static LicenseData? LicenseData => licenseData.Value;
|
||||
private static readonly object _licenseLock = new();
|
||||
private static LicenseData? _cachedLicenseData;
|
||||
private static string? _remoteClientLicenseKey;
|
||||
private static bool _isInitialized;
|
||||
|
||||
public static void SetRemoteClientLicenseKey(string? key)
|
||||
{
|
||||
lock (_licenseLock)
|
||||
{
|
||||
if (_remoteClientLicenseKey != key)
|
||||
{
|
||||
_remoteClientLicenseKey = key;
|
||||
_cachedLicenseData = null; // Force reload
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ReloadLicense()
|
||||
{
|
||||
lock (_licenseLock)
|
||||
{
|
||||
_cachedLicenseData = null;
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static LicenseData? LicenseData
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_licenseLock)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
_cachedLicenseData = LoadLicenseData();
|
||||
_isInitialized = true;
|
||||
}
|
||||
return _cachedLicenseData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static LicenseData? LoadLicenseData()
|
||||
{
|
||||
// Priority: File > Environment > ClientLicenseKey (server-provided)
|
||||
// Local license always takes precedence over server-provided license
|
||||
string? key = null;
|
||||
|
||||
// Check for a license file in the installation directory (highest priority)
|
||||
var keyfilepath = Path.Combine(UpdaterManager.INSTALLATIONDIR, "license.key");
|
||||
if (File.Exists(keyfilepath))
|
||||
key = $"file://{keyfilepath}";
|
||||
|
||||
// Check for a license key in the environment variables
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
key = Environment.GetEnvironmentVariable("DUPLICATI_LICENSE_KEY");
|
||||
|
||||
// Fall back to server-provided license key (lowest priority)
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
key = _remoteClientLicenseKey;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return null;
|
||||
|
||||
using var ct = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
return LicenseChecker.ObtainLicenseAsync(key, ct.Token).Await();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local license data (from file or environment only, ignoring server-provided license).
|
||||
/// Used for SystemInfo to report local license status.
|
||||
/// </summary>
|
||||
public static LicenseData? GetLocalLicenseData()
|
||||
{
|
||||
string? key = null;
|
||||
|
||||
// Check for a license file in the installation directory
|
||||
var keyfilepath = Path.Combine(UpdaterManager.INSTALLATIONDIR, "license.key");
|
||||
if (File.Exists(keyfilepath))
|
||||
key = $"file://{keyfilepath}";
|
||||
|
||||
// Check for a license key in the environment variables
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
key = Environment.GetEnvironmentVariable("DUPLICATI_LICENSE_KEY");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var ct = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
return LicenseChecker.ObtainLicenseAsync(key, ct.Token).Await();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote (server-provided) license data only.
|
||||
/// Used for OnConnect metadata to report remote license status.
|
||||
/// </summary>
|
||||
public static LicenseData? GetRemoteLicenseData()
|
||||
{
|
||||
string? key;
|
||||
lock (_licenseLock)
|
||||
{
|
||||
key = _remoteClientLicenseKey;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var ct = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
return LicenseChecker.ObtainLicenseAsync(key, ct.Token).Await();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static int AvailableOffice365UserSeats => GetFeatureSeats(DuplicatiLicenseFeatures.Office365Users);
|
||||
public static int AvailableOffice365GroupSeats => GetFeatureSeats(DuplicatiLicenseFeatures.Office365Groups);
|
||||
@@ -54,26 +178,4 @@ public static class LicenseHelper
|
||||
|
||||
return seats;
|
||||
}
|
||||
|
||||
private static Lazy<LicenseData?> licenseData = new Lazy<LicenseData?>(() =>
|
||||
{
|
||||
string? key = null;
|
||||
|
||||
// Check for a license file in the installation directory
|
||||
var keyfilepath = Path.Combine(UpdaterManager.INSTALLATIONDIR, "license.key");
|
||||
if (File.Exists(keyfilepath))
|
||||
key = $"file://{keyfilepath}";
|
||||
|
||||
// Check for a license key in the environment variables
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
key = Environment.GetEnvironmentVariable("DUPLICATI_LICENSE_KEY");
|
||||
|
||||
// No license key found
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return null;
|
||||
|
||||
using var ct = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
return LicenseChecker.ObtainLicenseAsync(key, ct.Token).Await();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user