269 lines
11 KiB
C#
269 lines
11 KiB
C#
// Copyright (C) 2024, The Duplicati Team
|
|
// https://duplicati.com, hello@duplicati.com
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using System.Net.Http.Json;
|
|
using Duplicati.Library.Logging;
|
|
using Duplicati.Library.Utility;
|
|
|
|
namespace Duplicati.Library.RemoteControl;
|
|
|
|
/// <summary>
|
|
/// Implementation of the remote control enrollment process
|
|
/// </summary>
|
|
public class RegisterForRemote : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// The log tag for messages from this class
|
|
/// </summary>
|
|
private static readonly string LogTag = Log.LogTagFromType<RegisterForRemote>();
|
|
|
|
/// <summary>
|
|
/// The interval between retries when registering the machine
|
|
/// </summary>
|
|
private static readonly TimeSpan ClientRegisterRetryInterval = TimeSpan.FromSeconds(30);
|
|
/// <summary>
|
|
/// The maximum number of retries when registering the machine
|
|
/// </summary>
|
|
private static readonly int ClientRegisterMaxRetries = 10;
|
|
|
|
/// <summary>
|
|
/// A random ID for this client instance
|
|
/// </summary>
|
|
private static readonly string ClientInstanceId = Guid.NewGuid().ToString();
|
|
|
|
/// <summary>
|
|
/// The states that the process can be in
|
|
/// </summary>
|
|
public enum States
|
|
{
|
|
/// <summary>
|
|
/// The process has not started
|
|
/// </summary>
|
|
NotStarted,
|
|
/// <summary>
|
|
/// The machine is being registered
|
|
/// </summary>
|
|
Registering,
|
|
/// <summary>
|
|
/// The machine has been registered
|
|
/// </summary>
|
|
Registered,
|
|
/// <summary>
|
|
/// The machine is waiting for the user to claim it
|
|
/// </summary>
|
|
WaitingForClaim,
|
|
/// <summary>
|
|
/// The machine has been claimed
|
|
/// </summary>
|
|
Claimed,
|
|
/// <summary>
|
|
/// The process has failed
|
|
/// </summary>
|
|
Failed,
|
|
/// <summary>
|
|
/// The process has been disposed
|
|
/// </summary>
|
|
Disposed
|
|
}
|
|
|
|
/// <summary>
|
|
/// Data returned when the machine is claimed
|
|
/// </summary>
|
|
/// <param name="Success">True if the claim was successful</param>
|
|
/// <param name="StatusMessage">The status message for the claim</param>
|
|
/// <param name="JWT">The JWT token for the machine</param>
|
|
/// <param name="ServerUrl">The URL for the remote server</param>
|
|
/// <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>
|
|
private sealed record EnvelopedClaimedClientData(
|
|
bool Success,
|
|
string StatusMessage,
|
|
string JWT,
|
|
string ServerUrl,
|
|
string CertificateUrl,
|
|
IEnumerable<MiniServerCertificate> ServerCertificates,
|
|
string? LocalEncryptionKey
|
|
);
|
|
|
|
/// <summary>
|
|
/// The current state of the registration process
|
|
/// </summary>
|
|
private States _state;
|
|
/// <summary>
|
|
/// The URL to register the machine with
|
|
/// </summary>
|
|
private readonly string _registrationUrl;
|
|
/// <summary>
|
|
/// The cancellation token source for the process
|
|
/// </summary>
|
|
private readonly CancellationTokenSource _cancellationTokenSource;
|
|
/// <summary>
|
|
/// The HTTP client to use for requests
|
|
/// </summary>
|
|
private readonly HttpClient _httpClient;
|
|
|
|
/// <summary>
|
|
/// The data returned when registering the machine
|
|
/// </summary>
|
|
private RegisterClientData? _registerClientData;
|
|
/// <summary>
|
|
/// The data returned when claiming the machine
|
|
/// </summary>
|
|
private ClaimedClientData? _claimedClientData;
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of the registration process
|
|
/// </summary>
|
|
/// <param name="registrationUrl">The URL to register the machine with</param>
|
|
/// <param name="httpClient">The HTTP client to use for requests</param>
|
|
/// <param name="cancellationToken">The cancellation token to use for the process</param>
|
|
public RegisterForRemote(string registrationUrl, HttpClient? httpClient, CancellationToken cancellationToken)
|
|
{
|
|
_state = States.NotStarted;
|
|
_registrationUrl = registrationUrl;
|
|
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
_httpClient = httpClient ?? new HttpClient();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The current state of the registration process
|
|
/// </summary>
|
|
public States State => _state;
|
|
|
|
/// <summary>
|
|
/// Starts the registration process and returns the data needed to claim the machine
|
|
/// </summary>
|
|
/// <param name="maxRetries">The maximum number of retries when registering the machine</param>
|
|
/// <param name="retryInterval">The interval between retries when registering the machine</param>
|
|
/// <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)
|
|
{
|
|
if (_state != States.NotStarted)
|
|
throw new InvalidOperationException("Registration process has already started");
|
|
|
|
_state = States.Registering;
|
|
try
|
|
{
|
|
_registerClientData = await RetryHelper.Retry(() => RegisterClient(), maxRetries ?? ClientRegisterMaxRetries, retryInterval ?? ClientRegisterRetryInterval, _cancellationTokenSource.Token);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_state != States.Disposed)
|
|
_state = States.Failed;
|
|
Log.WriteMessage(LogMessageType.Error, LogTag, "ClientRegistrationFailed", ex, $"Failed to register machine with server: {_registrationUrl}");
|
|
throw;
|
|
}
|
|
|
|
|
|
if (_state != States.Disposed)
|
|
_state = States.Registered;
|
|
return _registerClientData!;
|
|
}
|
|
|
|
private JsonContent CreateMachineData()
|
|
=> JsonContent.Create(new
|
|
{
|
|
InstanceId = ClientInstanceId,
|
|
MachineId = AutoUpdater.UpdaterManager.MachineID,
|
|
InstallId = AutoUpdater.UpdaterManager.InstallID,
|
|
LocalTime = DateTimeOffset.Now,
|
|
Version = AutoUpdater.UpdaterManager.SelfVersion?.Version,
|
|
PackageTypeId = AutoUpdater.UpdaterManager.PackageTypeId
|
|
});
|
|
|
|
/// <summary>
|
|
/// Registers the machine with the server
|
|
/// </summary>
|
|
/// <returns>The data needed to claim the machine</returns>
|
|
private async Task<RegisterClientData> RegisterClient()
|
|
{
|
|
var response = await _httpClient.PostAsync(_registrationUrl, JsonContent.Create(new
|
|
{
|
|
InstanceId = ClientInstanceId,
|
|
MachineId = AutoUpdater.UpdaterManager.MachineID,
|
|
InstallId = AutoUpdater.UpdaterManager.InstallID,
|
|
LocalTime = DateTimeOffset.Now
|
|
}), _cancellationTokenSource.Token);
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<RegisterClientData>()
|
|
?? throw new Exception("Failed to read client registration data");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Claims the machine with the server
|
|
/// </summary>
|
|
/// <returns>The settings for 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<ClaimedClientData> Claim()
|
|
{
|
|
if (_state != States.Registered)
|
|
throw new InvalidOperationException("Resgistration process is not in the registered state");
|
|
|
|
_state = States.WaitingForClaim;
|
|
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(Math.Min(3600, Math.Max(1, _registerClientData!.MaxLifetimeSeconds))));
|
|
try
|
|
{
|
|
_claimedClientData = await RetryHelper.Retry(() => CheckClientClaimed(), Math.Min(100, Math.Max(1, _registerClientData!.MaxRetries)), TimeSpan.FromSeconds(_registerClientData!.RetrySeconds), _cancellationTokenSource.Token);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_state != States.Disposed)
|
|
_state = States.Failed;
|
|
Log.WriteMessage(LogMessageType.Error, LogTag, "ClientClaimFailed", ex, "Failed to claim machine with server");
|
|
throw;
|
|
}
|
|
|
|
_state = States.Claimed;
|
|
return _claimedClientData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the machine has been claimed
|
|
/// </summary>
|
|
/// <returns>The data for the claimed machine</returns>
|
|
/// <exception cref="Exception">Thrown if the machine could not be claimed</exception>
|
|
private async Task<ClaimedClientData> CheckClientClaimed()
|
|
{
|
|
var response = await _httpClient.PostAsync(_registerClientData!.StatusLink, CreateMachineData(), _cancellationTokenSource.Token);
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
var result = await response.Content.ReadFromJsonAsync<EnvelopedClaimedClientData>()
|
|
?? throw new Exception("Failed to read machine claim data");
|
|
|
|
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);
|
|
}
|
|
|
|
/// </inheritdoc>
|
|
public void Dispose()
|
|
{
|
|
_cancellationTokenSource.Cancel();
|
|
_httpClient.Dispose();
|
|
_state = States.Disposed;
|
|
}
|
|
} |