diff --git a/Duplicati/Agent/Program.cs b/Duplicati/Agent/Program.cs index 6d55f7328..feeca9f2d 100644 --- a/Duplicati/Agent/Program.cs +++ b/Duplicati/Agent/Program.cs @@ -85,7 +85,7 @@ public static class Program var runcmd = new Command("run", "Runs the agent") { - new Option("registration-url", description: "The server URL to connect to", getDefaultValue: () => "https://agent.duplicati.com"), + new Option("registration-url", description: "The server URL to connect to", getDefaultValue: () => RegisterForRemote.DefaultRegisterationUrl), new Option("agent-settings-file", description: "The file to use for the agent settings", getDefaultValue: () => new FileInfo(Settings.DefaultSettingsFile)), new Option("webservice-listen-interface", description: "The interface to listen on for the webserver", getDefaultValue: () => "loopback"), new Option("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"); diff --git a/Duplicati/Library/RemoteControl/RegisterForRemote.cs b/Duplicati/Library/RemoteControl/RegisterForRemote.cs index 344e7d9f0..751d19fdd 100644 --- a/Duplicati/Library/RemoteControl/RegisterForRemote.cs +++ b/Duplicati/Library/RemoteControl/RegisterForRemote.cs @@ -181,7 +181,7 @@ public class RegisterForRemote : IDisposable /// The data needed to claim the machine /// Thrown if the class is not in the correct state /// Thrown if the machine could not be registered - public async Task 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); } + /// + /// Creates the machine data content for registration + /// + /// The JSON content for the machine data private JsonContent CreateMachineData() - => JsonContent.Create(new + { + var basics = new Dictionary { - 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); + } /// /// Registers the machine with the server /// /// The data needed to claim the machine - private async Task 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(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(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(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"); } /// @@ -245,6 +281,9 @@ public class RegisterForRemote : IDisposable /// Thrown if the machine could not be registered public async Task 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"); diff --git a/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs b/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs index 8af2c2a85..d6df3389e 100644 --- a/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs +++ b/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs @@ -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();