Added support for remote control with pre-signed url

This commit is contained in:
Kenneth Skovhede
2024-10-16 16:28:13 +02:00
parent f251d34b57
commit 9db3cce5e2
3 changed files with 69 additions and 28 deletions
+3 -2
View File
@@ -85,7 +85,7 @@ public static class Program
var runcmd = new Command("run", "Runs the agent")
{
new Option<string>("registration-url", description: "The server URL to connect to", getDefaultValue: () => "https://agent.duplicati.com"),
new Option<string>("registration-url", description: "The server URL to connect to", getDefaultValue: () => RegisterForRemote.DefaultRegisterationUrl),
new Option<FileInfo>("agent-settings-file", description: "The file to use for the agent settings", getDefaultValue: () => new FileInfo(Settings.DefaultSettingsFile)),
new Option<string>("webservice-listen-interface", description: "The interface to listen on for the webserver", getDefaultValue: () => "loopback"),
new Option<string>("webservice-port", description: "The port to listen on for the webserver", getDefaultValue: () => "8210"),
@@ -248,7 +248,8 @@ public static class Program
using (var registration = new RegisterForRemote(registrationUrl, null, cancellationToken))
{
var registerClientData = await registration.Register();
Log.WriteMessage(LogMessageType.Information, LogTag, "ClientRegistered", $"Machine registered, claim it by visiting: {registerClientData.ClaimLink}");
if (registerClientData.RegistrationData != null)
Log.WriteMessage(LogMessageType.Information, LogTag, "ClientRegistered", $"Machine registered, claim it by visiting: {registerClientData.RegistrationData.ClaimLink}");
var claimedClientData = await registration.Claim();
Log.WriteMessage(LogMessageType.Information, LogTag, "ClientClaimed", "Machine claimed, saving JWT");
@@ -181,7 +181,7 @@ public class RegisterForRemote : IDisposable
/// <returns>The data needed to claim the machine</returns>
/// <exception cref="InvalidOperationException">Thrown if the class is not in the correct state</exception>
/// <exception cref="Exception">Thrown if the machine could not be registered</exception>
public async Task<RegisterClientData> Register(int? maxRetries = null, TimeSpan? retryInterval = null)
public async Task<(RegisterClientData? RegistrationData, ClaimedClientData? ClaimedData)> Register(int? maxRetries = null, TimeSpan? retryInterval = null)
{
if (_state != States.NotStarted)
throw new InvalidOperationException("Registration process has already started");
@@ -189,7 +189,7 @@ public class RegisterForRemote : IDisposable
_state = States.Registering;
try
{
_registerClientData = await RetryHelper.Retry(() => RegisterClient(), maxRetries ?? ClientRegisterMaxRetries, retryInterval ?? ClientRegisterRetryInterval, _cancellationTokenSource.Token);
(_registerClientData, _claimedClientData) = await RetryHelper.Retry(() => RegisterClient(), maxRetries ?? ClientRegisterMaxRetries, retryInterval ?? ClientRegisterRetryInterval, _cancellationTokenSource.Token);
}
catch (Exception ex)
{
@@ -199,42 +199,78 @@ public class RegisterForRemote : IDisposable
throw;
}
if (_state != States.Disposed)
_state = States.Registered;
return _registerClientData!;
{
if (_claimedClientData != null)
_state = States.Claimed;
else
_state = States.Registered;
}
return (_registerClientData, _claimedClientData);
}
/// <summary>
/// Creates the machine data content for registration
/// </summary>
/// <returns>The JSON content for the machine data</returns>
private JsonContent CreateMachineData()
=> JsonContent.Create(new
{
var basics = new Dictionary<string, string?>
{
InstanceId = ClientInstanceId,
MachineId = AutoUpdater.UpdaterManager.MachineID,
InstallId = AutoUpdater.UpdaterManager.InstallID,
LocalTime = DateTimeOffset.Now,
Version = AutoUpdater.UpdaterManager.SelfVersion?.Version,
PackageTypeId = AutoUpdater.UpdaterManager.PackageTypeId
});
{ "instanceId", ClientInstanceId },
{ "machineId", AutoUpdater.UpdaterManager.MachineID },
{ "machineName", AutoUpdater.UpdaterManager.MachineName },
{ "installId", AutoUpdater.UpdaterManager.InstallID },
{ "localTime", DateTimeOffset.Now.ToString("o") },
{ "version", AutoUpdater.UpdaterManager.SelfVersion?.Version },
{ "packageTypeId", AutoUpdater.UpdaterManager.PackageTypeId }
};
// Pass any query parameters from the registration URL into the JSON body
// so they url can override the defaults if needed
var uri = new System.Uri(_registrationUrl);
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
foreach (var key in query.AllKeys)
if (key != null && !string.IsNullOrWhiteSpace(query[key]))
basics[key] = query[key];
return JsonContent.Create(basics);
}
/// <summary>
/// Registers the machine with the server
/// </summary>
/// <returns>The data needed to claim the machine</returns>
private async Task<RegisterClientData> RegisterClient()
private async Task<(RegisterClientData?, ClaimedClientData?)> RegisterClient()
{
var response = await _httpClient.PostAsync(_registrationUrl, JsonContent.Create(new
{
InstanceId = ClientInstanceId,
MachineId = AutoUpdater.UpdaterManager.MachineID,
InstallId = AutoUpdater.UpdaterManager.InstallID,
PackageTypeId = AutoUpdater.UpdaterManager.PackageTypeId,
LocalTime = DateTimeOffset.Now,
}), _cancellationTokenSource.Token);
var response = await _httpClient.PostAsync(_registrationUrl, CreateMachineData(), _cancellationTokenSource.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<RegisterClientData>(options: JsonOptions, _cancellationTokenSource.Token)
?? throw new Exception("Failed to read client registration data");
var raw = await response.Content.ReadAsStringAsync(_cancellationTokenSource.Token);
if (string.IsNullOrWhiteSpace(raw))
throw new Exception("Failed to read client registration data");
var doc = JsonDocument.Parse(raw);
if (doc.RootElement.TryGetProperty("jwt", out var _) && doc.RootElement.TryGetProperty("serverUrl", out var _))
{
return JsonSerializer.Deserialize<ClaimedClientData>(raw, JsonOptions) switch
{
{ } data => (null, data),
null => throw new Exception("Failed to read client claimed data")
};
}
if (doc.RootElement.TryGetProperty("claimLink", out var _) && doc.RootElement.TryGetProperty("statusLink", out var _))
{
return JsonSerializer.Deserialize<RegisterClientData>(raw, JsonOptions) switch
{
{ } data => (data, null),
null => throw new Exception("Failed to read client registration data")
};
}
throw new Exception("Failed to read client registration data");
}
/// <summary>
@@ -245,6 +281,9 @@ public class RegisterForRemote : IDisposable
/// <exception cref="Exception">Thrown if the machine could not be registered</exception>
public async Task<ClaimedClientData> Claim()
{
if (_state == States.Claimed)
return _claimedClientData ?? throw new InvalidOperationException("Claimed data is null");
if (_state != States.Registered)
throw new InvalidOperationException("Resgistration process is not in the registered state");
@@ -104,7 +104,8 @@ public class RemoteControllerRegistrationService(Connection connection, IHttpCli
var data = await _registerForRemote.Register(maxRetries: 3, retryInterval: TimeSpan.FromSeconds(5));
// Make the link visible to outside
RegistrationUrl = data.ClaimLink;
if (data.RegistrationData != null)
RegistrationUrl = data.RegistrationData.ClaimLink;
// Grab the claim data once it is returned
var claimData = await _registerForRemote.Claim();