Merge branch 'master' into fix/maintenance-write-path-config-persistence
This commit is contained in:
@@ -319,6 +319,8 @@ public static class Program
|
||||
settings.JWT,
|
||||
settings.CertificateUrl,
|
||||
settings.ServerCertificates,
|
||||
refreshSettingsBy: null,
|
||||
forceConnect: true,
|
||||
cts.Token,
|
||||
OnConnect,
|
||||
m => ReKey(applicationSettings, m, agentConfig),
|
||||
@@ -450,6 +452,10 @@ public static class Program
|
||||
};
|
||||
|
||||
settings.Save();
|
||||
|
||||
// If settings are present in the claim data, apply them via the control handler
|
||||
if (claimedClientData.Settings != null && claimedClientData.Settings.Count > 0)
|
||||
await OnControl(KeepRemoteConnection.ControlMessage.CreateSettingsControlMessage(claimedClientData.Settings));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -313,14 +313,14 @@ internal class RemoteSynchronizationHandler : IDisposable
|
||||
if (string.IsNullOrWhiteSpace(dest.Config.Dst))
|
||||
continue;
|
||||
|
||||
if (!ShouldTriggerSync(i, dest))
|
||||
{
|
||||
Logging.Log.WriteInformationMessage(LOGTAG, "RemoteSyncSkipped", "Remote synchronization to {0} skipped due to trigger mode conditions not met.", dest);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!ShouldTriggerSync(i, dest))
|
||||
{
|
||||
Logging.Log.WriteInformationMessage(LOGTAG, "RemoteSyncSkipped", "Remote synchronization to {0} skipped due to trigger mode conditions not met.", dest);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update progress to show sync is in progress
|
||||
m_results.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_RemoteSynchronization);
|
||||
m_results.OperationProgressUpdater.UpdateRemoteSyncDestination(i + 1, m_destinations.Count);
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="MailKit" Version="4.12.1" />
|
||||
<PackageReference Include="MimeKit" Version="4.15.1" />
|
||||
<PackageReference Include="MailKit" Version="4.16.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.16.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -124,6 +124,10 @@ public sealed record ControlRequestMessage(string Command, Dictionary<string, st
|
||||
/// The key that contains the applied settings version
|
||||
/// </summary>
|
||||
public const string SettingsVersionKey = "settingsversion";
|
||||
/// <summary>
|
||||
/// The key that contains the refresh settings by key
|
||||
/// </summary>
|
||||
public const string RefreshSettingsByKey = "refreshsettingsby";
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
|
||||
@@ -174,6 +174,10 @@ public class KeepRemoteConnection : IDisposable
|
||||
/// The last time a message was received
|
||||
/// </summary>
|
||||
private DateTimeOffset _lastMessageReceived = DateTimeOffset.MinValue;
|
||||
/// <summary>
|
||||
/// The time to refresh the settings by, or null if keeping a persistent connection
|
||||
/// </summary>
|
||||
private DateTimeOffset _refreshSettingsBy = DateTimeOffset.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new connection to the remote server
|
||||
@@ -182,6 +186,8 @@ public class KeepRemoteConnection : IDisposable
|
||||
/// <param name="JWT">The JWT token to use</param>
|
||||
/// <param name="certificateUrl">The certificate url to use</param>
|
||||
/// <param name="serverKeys">The server keys to use</param>
|
||||
/// <param name="refreshSettingsBy">The time to refresh the settings by, or null if keeping a persistent connection</param>
|
||||
/// <param name="forceConnect">If the connection should be force enabled, ignoring re-connect delays</param>
|
||||
/// <param name="cancellationToken">The token to cancel the connection</param>
|
||||
/// <param name="onConnect">The callback to call when connecting</param>
|
||||
/// <param name="onReKey">The callback to call when rekeying</param>
|
||||
@@ -192,6 +198,8 @@ public class KeepRemoteConnection : IDisposable
|
||||
string JWT,
|
||||
string certificateUrl,
|
||||
IEnumerable<MiniServerCertificate> serverKeys,
|
||||
DateTimeOffset? refreshSettingsBy,
|
||||
bool forceConnect,
|
||||
CancellationToken cancellationToken,
|
||||
Func<Dictionary<string, string?>, Task<Dictionary<string, string?>>> onConnect,
|
||||
Func<ClaimedClientData, Task> onReKey,
|
||||
@@ -207,15 +215,17 @@ public class KeepRemoteConnection : IDisposable
|
||||
_onReKey = onReKey;
|
||||
_onControl = onControl;
|
||||
_onMessage = onMessage;
|
||||
_refreshSettingsBy = refreshSettingsBy ?? DateTimeOffset.MinValue;
|
||||
|
||||
_client = new Websocket.Client.WebsocketClient(new Uri(serverUrl));
|
||||
_runnerTask = RunMainLoop();
|
||||
_runnerTask = RunMainLoop(forceConnect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the inner loop of the connection
|
||||
/// </summary>
|
||||
private async Task RunMainLoop()
|
||||
/// <param name="forceConnect">If the connection should be force enabled, ignoring re-connect delays</param>
|
||||
private async Task RunMainLoop(bool forceConnect)
|
||||
{
|
||||
_client.ReconnectTimeout = NoResponseTimeout;
|
||||
_client.LostReconnectTimeout = ReconnectInterval;
|
||||
@@ -236,8 +246,8 @@ public class KeepRemoteConnection : IDisposable
|
||||
TimeSpan.FromSeconds(1),
|
||||
_ =>
|
||||
{
|
||||
// Reconnect if we have disconnected
|
||||
if (!_client.IsRunning)
|
||||
// Reconnect if we have disconnected (but not if auto-reconnect is disabled)
|
||||
if (!_client.IsRunning && !IsAutoReconnectDisabled())
|
||||
{
|
||||
reconnectHelper.Signal();
|
||||
}
|
||||
@@ -274,8 +284,15 @@ public class KeepRemoteConnection : IDisposable
|
||||
_state = ConnectionState.NotConnected;
|
||||
_serverCertificate = null;
|
||||
_serverPublicKey = null;
|
||||
reconnectHelper.Signal();
|
||||
Log.WriteMessage(LogMessageType.Warning, LogTag, "WebsocketDisconnect", "Disconnected from the server");
|
||||
if (!IsAutoReconnectDisabled())
|
||||
{
|
||||
reconnectHelper.Signal();
|
||||
Log.WriteMessage(LogMessageType.Warning, LogTag, "WebsocketDisconnect", "Disconnected from the server");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.WriteMessage(LogMessageType.Information, LogTag, "WebsocketDisconnect", "Disconnected from the server. Auto-reconnect disabled until {0}", _refreshSettingsBy.ToLocalTime());
|
||||
}
|
||||
});
|
||||
|
||||
_client.MessageReceived.Subscribe(async msg =>
|
||||
@@ -402,7 +419,12 @@ public class KeepRemoteConnection : IDisposable
|
||||
case MessageType.Control:
|
||||
await _onControl(new ControlMessage(
|
||||
envelope.GetPayload<ControlRequestMessage>(),
|
||||
response => SendEnvelope(envelope.RespondWith(response))
|
||||
response => SendEnvelope(envelope.RespondWith(response)),
|
||||
refreshSettingsBy =>
|
||||
{
|
||||
_refreshSettingsBy = DateTimeOffset.FromUnixTimeMilliseconds(Math.Max(refreshSettingsBy.ToUnixTimeMilliseconds(), DateTimeOffset.UtcNow.AddSeconds(30).ToUnixTimeMilliseconds()));
|
||||
_client.Stop(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, "Disconnect requested");
|
||||
}
|
||||
));
|
||||
break;
|
||||
|
||||
@@ -428,7 +450,8 @@ public class KeepRemoteConnection : IDisposable
|
||||
});
|
||||
|
||||
// Start the connection
|
||||
reconnectHelper.Signal();
|
||||
if (forceConnect || !IsAutoReconnectDisabled())
|
||||
reconnectHelper.Signal();
|
||||
|
||||
var t = await Task.WhenAny(
|
||||
heartbeatHelper.RunLoopAsync(),
|
||||
@@ -456,6 +479,8 @@ public class KeepRemoteConnection : IDisposable
|
||||
/// <param name="JWT">The JWT to use</param>
|
||||
/// <param name="certificateUrl">The certificate url to use</param>
|
||||
/// <param name="serverKeys">The server keys to use</param>
|
||||
/// <param name="refreshSettingsBy">The time to refresh settings by</param>
|
||||
/// <param name="forceConnect">If the connection should be force enabled, ignoring re-connect delays</param>
|
||||
/// <param name="cancellationToken">The token to cancel the connection</param>
|
||||
/// <param name="onConnect">The callback to call when connecting</param>
|
||||
/// <param name="onReKey">The callback to call when rekeying</param>
|
||||
@@ -467,6 +492,8 @@ public class KeepRemoteConnection : IDisposable
|
||||
string JWT,
|
||||
string certificateUrl,
|
||||
IEnumerable<MiniServerCertificate> serverKeys,
|
||||
DateTimeOffset? refreshSettingsBy,
|
||||
bool forceConnect,
|
||||
CancellationToken cancellationToken,
|
||||
Func<Dictionary<string, string?>, Task<Dictionary<string, string?>>> onConnect,
|
||||
Func<ClaimedClientData, Task> onReKey,
|
||||
@@ -474,7 +501,7 @@ public class KeepRemoteConnection : IDisposable
|
||||
Func<CommandMessage, Task> onMessage)
|
||||
=> Task.Run(async () =>
|
||||
{
|
||||
using var connection = new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, cancellationToken, onConnect, onReKey, onControl, onMessage);
|
||||
using var connection = new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, refreshSettingsBy, forceConnect, cancellationToken, onConnect, onReKey, onControl, onMessage);
|
||||
await connection._runnerTask;
|
||||
});
|
||||
|
||||
@@ -537,6 +564,13 @@ public class KeepRemoteConnection : IDisposable
|
||||
/// </summary>
|
||||
public ConnectionState State => _state;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if automatic reconnection should be disabled based on the RefreshSettingsBy timestamp.
|
||||
/// </summary>
|
||||
/// <returns>True if automatic reconnect is disabled; otherwise, false.</returns>
|
||||
public bool IsAutoReconnectDisabled()
|
||||
=> DateTimeOffset.UtcNow < _refreshSettingsBy;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new connection to the remote server
|
||||
/// </summary>
|
||||
@@ -544,6 +578,9 @@ public class KeepRemoteConnection : IDisposable
|
||||
/// <param name="JWT">The JWT token to use</param>
|
||||
/// <param name="certificateUrl">The certificate url to use</param>
|
||||
/// <param name="serverKeys">The server keys to use</param>
|
||||
/// <param name="refreshSettingsBy">The timestamp to disable automatic reconnect</param>
|
||||
/// <param name="forceConnect">If the connection should be force enabled, ignoring re-connect delays</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use</param>
|
||||
/// <param name="onConnect">The callback to call when connecting</param>
|
||||
/// <param name="onReKey">The callback to call when rekeying</param>
|
||||
/// <param name="onControl">The callback to call when a control message is received</param>
|
||||
@@ -554,12 +591,14 @@ public class KeepRemoteConnection : IDisposable
|
||||
string JWT,
|
||||
string certificateUrl,
|
||||
IEnumerable<MiniServerCertificate> serverKeys,
|
||||
DateTimeOffset? refreshSettingsBy,
|
||||
bool forceConnect,
|
||||
CancellationToken cancellationToken,
|
||||
Func<Dictionary<string, string?>, Task<Dictionary<string, string?>>> onConnect,
|
||||
Func<ClaimedClientData, Task> onReKey,
|
||||
Func<ControlMessage, Task> onControl,
|
||||
Func<CommandMessage, Task> onMessage)
|
||||
=> new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, cancellationToken, onConnect, onReKey, onControl, onMessage);
|
||||
=> new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, refreshSettingsBy, forceConnect, cancellationToken, onConnect, onReKey, onControl, onMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Requests a certificate refresh
|
||||
@@ -678,6 +717,10 @@ public class KeepRemoteConnection : IDisposable
|
||||
/// </summary>
|
||||
private readonly Func<ControlResponseMessage, bool> _respondCommand;
|
||||
/// <summary>
|
||||
/// The callback method to request disconnect after responding
|
||||
/// </summary>
|
||||
private readonly Action<DateTimeOffset>? _requestDisconnect;
|
||||
/// <summary>
|
||||
/// The command request message
|
||||
/// </summary>
|
||||
public ControlRequestMessage ControlRequestMessage { get; }
|
||||
@@ -687,10 +730,12 @@ public class KeepRemoteConnection : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="controlRequestMessage">The command request message</param>
|
||||
/// <param name="respondCommand">The callback method that will receive the response</param>
|
||||
public ControlMessage(ControlRequestMessage controlRequestMessage, Func<ControlResponseMessage, bool> respondCommand)
|
||||
/// <param name="requestDisconnect">Optional callback to request disconnect after responding</param>
|
||||
public ControlMessage(ControlRequestMessage controlRequestMessage, Func<ControlResponseMessage, bool> respondCommand, Action<DateTimeOffset>? requestDisconnect)
|
||||
{
|
||||
ControlRequestMessage = controlRequestMessage;
|
||||
_respondCommand = respondCommand;
|
||||
_requestDisconnect = requestDisconnect;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -700,5 +745,26 @@ public class KeepRemoteConnection : IDisposable
|
||||
/// <returns>True if the response was sent</returns>
|
||||
public bool Respond(ControlResponseMessage response)
|
||||
=> _respondCommand(response);
|
||||
|
||||
/// <summary>
|
||||
/// Requests the connection to be closed after responding.
|
||||
/// This is used when the server indicates the client should disconnect
|
||||
/// </summary>
|
||||
/// <param name="refreshSettingsBy">The time to refresh settings by</param>
|
||||
public void RequestDisconnect(DateTimeOffset refreshSettingsBy)
|
||||
=> _requestDisconnect?.Invoke(refreshSettingsBy);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a control message for initial settings from ClaimedClientData.
|
||||
/// This is used to apply settings that are returned when the machine is claimed.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings dictionary from ClaimedClientData.Settings</param>
|
||||
/// <returns>A control message with the settings as parameters</returns>
|
||||
public static ControlMessage CreateSettingsControlMessage(Dictionary<string, string?> settings)
|
||||
=> new(
|
||||
new ControlRequestMessage(ControlRequestMessage.UpdateSettingsCommand, settings),
|
||||
_ => true, // No-op response handler since there's no websocket connection for this
|
||||
_ => { } // No-op disconnect handler since there's no websocket connection for this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ public class RegisterForRemote : IDisposable
|
||||
/// <param name="CertificateUrl">The URL for getting new server certificates</param>
|
||||
/// <param name="ServerCertificates">The certificates for the remote server</param>
|
||||
/// <param name="LocalEncryptionKey">The encryption key for the local settings</param>
|
||||
/// <param name="Settings">The settings for the machine</param>
|
||||
private sealed record EnvelopedClaimedClientData(
|
||||
bool Success,
|
||||
string StatusMessage,
|
||||
@@ -140,7 +141,8 @@ public class RegisterForRemote : IDisposable
|
||||
string ServerUrl,
|
||||
string CertificateUrl,
|
||||
IEnumerable<MiniServerCertificate> ServerCertificates,
|
||||
string? LocalEncryptionKey
|
||||
string? LocalEncryptionKey,
|
||||
Dictionary<string, string?>? Settings
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@@ -338,7 +340,7 @@ public class RegisterForRemote : IDisposable
|
||||
if (!result.Success)
|
||||
throw new Exception($"Failed to claim machine: {result.StatusMessage}");
|
||||
|
||||
return new ClaimedClientData(result.JWT, result.ServerUrl, result.CertificateUrl, result.ServerCertificates, result.LocalEncryptionKey);
|
||||
return new ClaimedClientData(result.JWT, result.ServerUrl, result.CertificateUrl, result.ServerCertificates, result.LocalEncryptionKey, result.Settings);
|
||||
}
|
||||
|
||||
/// </inheritdoc>
|
||||
|
||||
@@ -47,12 +47,14 @@ public sealed record RegisterClientData(
|
||||
/// <param name="CertificateUrl">The URL for getting new server certificates</param>
|
||||
/// <param name="ServerCertificates">The certificates for the remote server</param>
|
||||
/// <param name="LocalEncryptionKey">The encryption key for the local settings</param>
|
||||
/// <param name="Settings">The settings for the machine, same as in ControlRequestMessage.Parameters</param>
|
||||
public sealed record ClaimedClientData(
|
||||
string JWT,
|
||||
string ServerUrl,
|
||||
string CertificateUrl,
|
||||
IEnumerable<MiniServerCertificate> ServerCertificates,
|
||||
string? LocalEncryptionKey
|
||||
string? LocalEncryptionKey,
|
||||
Dictionary<string, string?>? Settings = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -266,21 +266,26 @@ namespace Duplicati.Server.Database
|
||||
lock (m_lock)
|
||||
{
|
||||
var tr = transaction ?? m_connection.BeginTransaction();
|
||||
OverwriteAndUpdateDb(
|
||||
tr,
|
||||
cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Metadata"" WHERE ""BackupID"" = @Id")
|
||||
.SetParameterValue("@Id", id),
|
||||
values ?? new Dictionary<string, string>(),
|
||||
cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (@BackupId, @Name, @Value)"),
|
||||
(cmd, f) => cmd.SetParameterValue("@BackupId", id)
|
||||
.SetParameterValue("@Name", f.Key)
|
||||
.SetParameterValue("@Value", f.Value)
|
||||
);
|
||||
|
||||
if (transaction == null)
|
||||
try
|
||||
{
|
||||
tr.Commit();
|
||||
tr.Dispose();
|
||||
OverwriteAndUpdateDb(
|
||||
tr,
|
||||
cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Metadata"" WHERE ""BackupID"" = @Id")
|
||||
.SetParameterValue("@Id", id),
|
||||
values ?? new Dictionary<string, string>(),
|
||||
cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (@BackupId, @Name, @Value)"),
|
||||
(cmd, f) => cmd.SetParameterValue("@BackupId", id)
|
||||
.SetParameterValue("@Name", f.Key)
|
||||
.SetParameterValue("@Value", f.Value)
|
||||
);
|
||||
|
||||
if (transaction == null)
|
||||
tr.Commit();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (transaction == null)
|
||||
tr.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,22 +310,27 @@ namespace Duplicati.Server.Database
|
||||
lock (m_lock)
|
||||
{
|
||||
var tr = transaction ?? m_connection.BeginTransaction();
|
||||
OverwriteAndUpdateDb(
|
||||
tr,
|
||||
cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Filter"" WHERE ""BackupID"" = @Id")
|
||||
.SetParameterValue("@Id", id),
|
||||
values,
|
||||
cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (@Id, @Order, @Include, @Expression)"),
|
||||
(cmd, f) => cmd.SetParameterValue("@Id", id)
|
||||
.SetParameterValue("@Order", f.Order)
|
||||
.SetParameterValue("@Include", f.Include)
|
||||
.SetParameterValue("@Expression", f.Expression)
|
||||
);
|
||||
|
||||
if (transaction == null)
|
||||
try
|
||||
{
|
||||
tr.Commit();
|
||||
tr.Dispose();
|
||||
OverwriteAndUpdateDb(
|
||||
tr,
|
||||
cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Filter"" WHERE ""BackupID"" = @Id")
|
||||
.SetParameterValue("@Id", id),
|
||||
values,
|
||||
cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (@Id, @Order, @Include, @Expression)"),
|
||||
(cmd, f) => cmd.SetParameterValue("@Id", id)
|
||||
.SetParameterValue("@Order", f.Order)
|
||||
.SetParameterValue("@Include", f.Include)
|
||||
.SetParameterValue("@Expression", f.Expression)
|
||||
);
|
||||
|
||||
if (transaction == null)
|
||||
tr.Commit();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (transaction == null)
|
||||
tr.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,36 +379,41 @@ namespace Duplicati.Server.Database
|
||||
lock (m_lock)
|
||||
{
|
||||
var tr = transaction ?? m_connection.BeginTransaction();
|
||||
if (m_encryptSensitiveFields)
|
||||
values = values.Select(x => new Setting
|
||||
{
|
||||
Filter = x.Filter,
|
||||
Name = x.Name,
|
||||
Value = EncryptSensitiveFields(x.Name, x.Value, m_key)
|
||||
}).ToList();
|
||||
|
||||
OverwriteAndUpdateDb(
|
||||
tr,
|
||||
cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Option"" WHERE ""BackupID"" = @Id")
|
||||
.SetParameterValue("@Id", id),
|
||||
values,
|
||||
cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (@BackupId, @Filter, @Name, @Value)"),
|
||||
(cmd, f) =>
|
||||
{
|
||||
if (IsPasswordPlaceholder(f.Value))
|
||||
throw new Exception("Attempted to save a property with the placeholder password");
|
||||
|
||||
cmd.SetParameterValue("@BackupId", id)
|
||||
.SetParameterValue("@Filter", f.Filter ?? "")
|
||||
.SetParameterValue("@Name", f.Name ?? "")
|
||||
.SetParameterValue("@Value", f.Value ?? "");
|
||||
}
|
||||
);
|
||||
|
||||
if (transaction == null)
|
||||
try
|
||||
{
|
||||
tr.Commit();
|
||||
tr.Dispose();
|
||||
if (m_encryptSensitiveFields)
|
||||
values = values.Select(x => new Setting
|
||||
{
|
||||
Filter = x.Filter,
|
||||
Name = x.Name,
|
||||
Value = EncryptSensitiveFields(x.Name, x.Value, m_key)
|
||||
}).ToList();
|
||||
|
||||
OverwriteAndUpdateDb(
|
||||
tr,
|
||||
cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Option"" WHERE ""BackupID"" = @Id")
|
||||
.SetParameterValue("@Id", id),
|
||||
values,
|
||||
cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (@BackupId, @Filter, @Name, @Value)"),
|
||||
(cmd, f) =>
|
||||
{
|
||||
if (IsPasswordPlaceholder(f.Value))
|
||||
throw new Exception("Attempted to save a property with the placeholder password");
|
||||
|
||||
cmd.SetParameterValue("@BackupId", id)
|
||||
.SetParameterValue("@Filter", f.Filter ?? "")
|
||||
.SetParameterValue("@Name", f.Name ?? "")
|
||||
.SetParameterValue("@Value", f.Value ?? "");
|
||||
}
|
||||
);
|
||||
|
||||
if (transaction == null)
|
||||
tr.Commit();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (transaction == null)
|
||||
tr.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,29 +460,34 @@ namespace Duplicati.Server.Database
|
||||
lock (m_lock)
|
||||
{
|
||||
var tr = transaction ?? m_connection.BeginTransaction();
|
||||
OverwriteAndUpdateDb(
|
||||
tr,
|
||||
cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Source"" WHERE ""BackupID"" = @Id")
|
||||
.SetParameterValue("@Id", id),
|
||||
values,
|
||||
cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (@BackupId, @Path)"),
|
||||
(cmd, f) =>
|
||||
{
|
||||
if (SourceMasking.IsSpecialSource(f) && UrlContainsPasswordPlaceholder(SourceMasking.ExtractUrl(f)))
|
||||
throw new Exception("Attempted to save a source with a placeholder password");
|
||||
|
||||
cmd.SetParameterValue("@BackupId", id)
|
||||
.SetParameterValue("@Path",
|
||||
m_encryptSensitiveFields && SourceMasking.IsSpecialSource(f)
|
||||
? EncryptedFieldHelper.Encrypt(f, m_key)
|
||||
: f);
|
||||
}
|
||||
);
|
||||
|
||||
if (transaction == null)
|
||||
try
|
||||
{
|
||||
tr.Commit();
|
||||
tr.Dispose();
|
||||
OverwriteAndUpdateDb(
|
||||
tr,
|
||||
cmd => cmd.SetCommandAndParameters(@"DELETE FROM ""Source"" WHERE ""BackupID"" = @Id")
|
||||
.SetParameterValue("@Id", id),
|
||||
values,
|
||||
cmd => cmd.SetCommandAndParameters(@"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (@BackupId, @Path)"),
|
||||
(cmd, f) =>
|
||||
{
|
||||
if (SourceMasking.IsSpecialSource(f) && UrlContainsPasswordPlaceholder(SourceMasking.ExtractUrl(f)))
|
||||
throw new Exception("Attempted to save a source with a placeholder password");
|
||||
|
||||
cmd.SetParameterValue("@BackupId", id)
|
||||
.SetParameterValue("@Path",
|
||||
m_encryptSensitiveFields && SourceMasking.IsSpecialSource(f)
|
||||
? EncryptedFieldHelper.Encrypt(f, m_key)
|
||||
: f);
|
||||
}
|
||||
);
|
||||
|
||||
if (transaction == null)
|
||||
tr.Commit();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (transaction == null)
|
||||
tr.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ public interface IRemoteController
|
||||
/// <summary>
|
||||
/// Enable the remote controller
|
||||
/// </summary>
|
||||
public void Enable();
|
||||
/// <param name="forceConnect">If the connection should be force enabled, ignoring re-connect delays</param>
|
||||
public void Enable(bool forceConnect);
|
||||
|
||||
/// <summary>
|
||||
/// Disable the remote controller
|
||||
|
||||
@@ -398,7 +398,7 @@ public class DuplicatiWebserver
|
||||
});
|
||||
|
||||
if (connection.ApplicationSettings.RemoteControlEnabled)
|
||||
app.Services.GetRequiredService<IRemoteController>().Enable();
|
||||
app.Services.GetRequiredService<IRemoteController>().Enable(false);
|
||||
|
||||
// Preload static system info, for better first-load experience
|
||||
_ = Task.Run(() => app.Services.GetRequiredService<ISystemInfoProvider>().GetSystemInfo(null));
|
||||
|
||||
@@ -106,7 +106,7 @@ public class RemoteControl : IEndpointV1
|
||||
|
||||
private static Dto.RemoteControlStatusOutput EnableRemoteControl(IRemoteControllerRegistration registration, IRemoteController remoteController)
|
||||
{
|
||||
remoteController.Enable();
|
||||
remoteController.Enable(true);
|
||||
return GetStatus(registration, remoteController);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ public class RemoteControllerHandler(Connection connection, IHttpClientFactory h
|
||||
await Task.CompletedTask;
|
||||
|
||||
Dictionary<string, string?>? result = null;
|
||||
var refreshSettingsBy = DateTimeOffset.MinValue;
|
||||
|
||||
Log.WriteMessage(LogMessageType.Verbose, LOGTAG, "OnControl", "Received control message: {0}", message);
|
||||
try
|
||||
@@ -141,6 +142,36 @@ public class RemoteControllerHandler(Connection connection, IHttpClientFactory h
|
||||
|
||||
connection.AddOrUpdateBackupAndSchedule(importData.Backup, importData.Schedule);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check if we need to delay the next reconnection
|
||||
var refreshSettingsByValue = message.ControlRequestMessage.Parameters.GetValueOrDefault(ControlRequestMessage.RefreshSettingsByKey);
|
||||
if (!string.IsNullOrWhiteSpace(refreshSettingsByValue) && long.TryParse(refreshSettingsByValue, out var refreshSettingsByLong) && refreshSettingsByLong > 0)
|
||||
{
|
||||
var target = DateTimeOffset.FromUnixTimeSeconds(refreshSettingsByLong);
|
||||
if (target > DateTimeOffset.UtcNow)
|
||||
refreshSettingsBy = target;
|
||||
|
||||
// Agent keeps its own config, and does not support connection delays
|
||||
if (applicationSettings.Origin != "Agent")
|
||||
{
|
||||
var connstr = connection.ApplicationSettings.RemoteControlConfig;
|
||||
if (!string.IsNullOrWhiteSpace(connstr))
|
||||
{
|
||||
var prevCfg = JsonConvert.DeserializeObject<RemoteControlConfig>(connstr);
|
||||
if (prevCfg != null)
|
||||
connection.ApplicationSettings.RemoteControlConfig = JsonConvert.SerializeObject(
|
||||
prevCfg with { RefreshSettingsBy = target }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.WriteMessage(LogMessageType.Error, LOGTAG, "OnControl", ex, "Failed to update refreshSettingsBy");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -150,7 +181,14 @@ public class RemoteControllerHandler(Connection connection, IHttpClientFactory h
|
||||
return;
|
||||
}
|
||||
|
||||
// Respond first, then disconnect if needed
|
||||
message.Respond(new ControlResponseMessage(true, result, null));
|
||||
|
||||
if (refreshSettingsBy > DateTimeOffset.UtcNow)
|
||||
{
|
||||
Log.WriteMessage(LogMessageType.Information, LOGTAG, "OnControl", "Requesting disconnect after responding to control message");
|
||||
message.RequestDisconnect(refreshSettingsBy);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ public sealed record RemoteControlConfig
|
||||
/// The trusted server certificates.
|
||||
/// </summary>
|
||||
public required IEnumerable<MiniServerCertificate> ServerCertificates { get; init; }
|
||||
/// <summary>
|
||||
/// The time when the settings should be refreshed.
|
||||
/// </summary>
|
||||
public DateTimeOffset? RefreshSettingsBy { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,7 +59,9 @@ public sealed record RemoteControlConfig
|
||||
/// </summary>
|
||||
/// <param name="connection">The database connection</param>
|
||||
/// <param name="httpClientFactory">The HTTP client factory</param>
|
||||
public class RemoteControllerRegistrationService(Connection connection, IHttpClientFactory httpClientFactory, IRemoteController remoteController) : IRemoteControllerRegistration
|
||||
/// <param name="remoteController">The remote controller</param>
|
||||
/// <param name="controllerHandler">The controller handler</param>
|
||||
public class RemoteControllerRegistrationService(Connection connection, IHttpClientFactory httpClientFactory, IRemoteController remoteController, IRemoteControllerHandler controllerHandler) : IRemoteControllerRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// The registration process controller this service is wrapping.
|
||||
@@ -117,9 +123,13 @@ public class RemoteControllerRegistrationService(Connection connection, IHttpCli
|
||||
ServerUrl = claimData.ServerUrl
|
||||
});
|
||||
|
||||
// If settings are present in the claim data, apply them via the control handler
|
||||
if (claimData.Settings != null && claimData.Settings.Count > 0)
|
||||
await controllerHandler.OnControl(KeepRemoteConnection.ControlMessage.CreateSettingsControlMessage(claimData.Settings));
|
||||
|
||||
// Automatically connect after we are registered
|
||||
if (remoteController.CanEnable)
|
||||
remoteController.Enable();
|
||||
remoteController.Enable(false);
|
||||
|
||||
});
|
||||
}
|
||||
@@ -141,5 +151,6 @@ public class RemoteControllerRegistrationService(Connection connection, IHttpCli
|
||||
_operationCancellation?.Dispose();
|
||||
_registerForRemote = null;
|
||||
_operationCancellation = null;
|
||||
RegistrationUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class RemoteControllerService(Connection connection, IRemoteControllerHan
|
||||
private KeepRemoteConnection? _keepRemoteConnection;
|
||||
|
||||
/// </inheritdoc>
|
||||
public void Enable()
|
||||
public void Enable(bool forceConnect)
|
||||
{
|
||||
if (_keepRemoteConnection != null)
|
||||
return;
|
||||
@@ -79,6 +79,8 @@ public class RemoteControllerService(Connection connection, IRemoteControllerHan
|
||||
config.Token,
|
||||
config.CertificateUrl,
|
||||
config.ServerCertificates,
|
||||
config.RefreshSettingsBy,
|
||||
forceConnect,
|
||||
CancellationToken.None,
|
||||
controllerHandler.OnConnect,
|
||||
controllerHandler.ReKey,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<ProjectReference Include="../../Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj" />
|
||||
<ProjectReference Include="../../Duplicati/Library/Utility/Duplicati.Library.Utility.csproj" />
|
||||
<ProjectReference Include="../LicenseChecker/Duplicati.Proprietary.LicenseChecker.csproj" />
|
||||
<PackageReference Include="MimeKit" Version="4.15.1" />
|
||||
<PackageReference Include="MimeKit" Version="4.16.0" />
|
||||
<PackageReference Include="jose-jwt" Version="5.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user