2025-01-17 14:00:14 -03:00
// Copyright (C) 2025, 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
2024-09-18 11:07:59 +02:00
// DEALINGS IN THE SOFTWARE.
using System.CommandLine ;
2024-10-24 15:56:39 +02:00
using System.CommandLine.Builder ;
2024-09-18 11:07:59 +02:00
using System.CommandLine.NamingConventionBinder ;
2024-10-24 15:56:39 +02:00
using System.CommandLine.Parsing ;
2024-09-18 11:07:59 +02:00
using System.Reactive.Linq ;
2024-09-27 11:19:10 +02:00
using System.Security.Cryptography ;
2025-01-28 13:01:35 +01:00
using Duplicati.Library.AutoUpdater ;
2024-10-24 15:56:39 +02:00
using Duplicati.Library.Encryption ;
using Duplicati.Library.Interface ;
2024-09-18 11:07:59 +02:00
using Duplicati.Library.Logging ;
using Duplicati.Library.Main ;
using Duplicati.Library.RemoteControl ;
using Duplicati.Library.RestAPI ;
2024-10-24 15:56:39 +02:00
using Duplicati.Library.Utility ;
2024-09-18 11:07:59 +02:00
using Duplicati.Server ;
using Duplicati.WebserverCore.Abstractions ;
using Microsoft.Extensions.DependencyInjection ;
namespace Duplicati.Agent ;
public static class Program
{
/// <summary>
/// The log tag for messages from this class
/// </summary>
private static readonly string LogTag = Log . LogTagFromType ( typeof ( Program ));
2024-09-27 11:19:10 +02:00
/// <summary>
/// Create a random pre-shared key for the agent
/// </summary>
private static string? PreSharedKey = null ;
2024-09-18 11:07:59 +02:00
/// <summary>
/// Commandline arguments for the agent
/// </summary>
2024-10-24 15:56:39 +02:00
/// <param name="AgentRegistrationUrl">The server URL to connect to</param>
2024-09-27 11:19:10 +02:00
/// <param name="AgentSettingsFile">The file to use for the agent settings</param>
2024-10-24 15:56:39 +02:00
/// <param name="AgentSettingsFilePassphrase">The passphrase for the agent settings file</param>
2024-09-27 11:19:10 +02:00
/// <param name="WebserviceListenInterface">The interface to listen on for the webserver</param>
/// <param name="WebservicePort">The port to listen on for the webserver</param>
/// <param name="WebservicePassword">The password for the webserver, random if none supplied</param>
2024-10-24 15:56:39 +02:00
/// <param name="SettingsEncryptionKey">The encryption key for the database settings</param>
2024-09-27 11:19:10 +02:00
/// <param name="WindowsEventLog">The Windows event log to write to</param>
/// <param name="DisableDbEncryption">Disable database encryption</param>
/// <param name="WebserviceResetJwtConfig">Reset the JWT configuration</param>
/// <param name="WebserviceAllowedHostnames">The allowed hostnames for the webserver</param>
/// <param name="WebserviceApiOnly">Only allow API access to the webserver</param>
/// <param name="WebserviceDisableSigninTokens">Disable signin tokens for the webserver</param>
/// <param name="PingPongKeepalive">Enable ping-pong keepalive</param>
/// <param name="DisablePreSharedKey">Disable the pre-shared key</param>
2024-12-05 12:20:11 +01:00
/// <param name="KeepWebservicePassword">Keep the webserver password</param>
2024-10-24 15:56:39 +02:00
/// <param name="SecretProvider">The secret provider to use</param>
/// <param name="SecretProviderCache">The secret provider cache level</param>
/// <param name="SecretProviderPattern">The secret provider pattern</param>
2024-09-18 11:07:59 +02:00
private sealed record CommandLineArguments (
2024-10-24 15:56:39 +02:00
string AgentRegistrationUrl ,
2024-09-18 11:07:59 +02:00
FileInfo AgentSettingsFile ,
2024-10-24 15:56:39 +02:00
string? AgentSettingsFilePassphrase ,
2024-10-25 15:03:01 +02:00
bool AgentRegisterOnly ,
2024-09-18 11:07:59 +02:00
string WebserviceListenInterface ,
string WebservicePort ,
string WebservicePassword ,
2024-10-24 15:56:39 +02:00
string? SettingsEncryptionKey ,
2024-09-18 11:07:59 +02:00
string WindowsEventLog ,
bool DisableDbEncryption ,
bool WebserviceResetJwtConfig ,
string WebserviceAllowedHostnames ,
bool WebserviceApiOnly ,
bool WebserviceDisableSigninTokens ,
2024-09-27 11:19:10 +02:00
bool PingPongKeepalive ,
2024-10-24 15:56:39 +02:00
bool DisablePreSharedKey ,
2024-12-05 12:20:11 +01:00
bool KeepWebservicePassword ,
2024-10-24 15:56:39 +02:00
string? SecretProvider ,
SecretProviderHelper . CachingLevel SecretProviderCache ,
string SecretProviderPattern
2024-09-18 11:07:59 +02:00
);
[STAThread]
public static Task < int > Main ( string [] args )
{
Library . AutoUpdater . PreloadSettingsLoader . ConfigurePreloadSettings ( ref args , Library . AutoUpdater . PackageHelper . NamedExecutable . Agent );
var runcmd = new Command ( "run" , "Runs the agent" )
{
2024-10-24 16:11:41 +02:00
new Option < string >( "--agent-registration-url" , description : "The server URL to connect to" , getDefaultValue : () => RegisterForRemote . DefaultRegisterationUrl ),
2024-10-24 15:56:39 +02:00
new Option < FileInfo >( "--agent-settings-file" , description : "The file to use for the agent settings" , getDefaultValue : () => new FileInfo ( Settings . DefaultSettingsFile )),
new Option < string? >( "--agent-settings-file-passphrase" , description : "The passphrase for the agent settings file" , getDefaultValue : () => null ),
2024-10-25 15:03:01 +02:00
new Option < bool >( "--agent-register-only" , description : "Only register the agent, then exit" , getDefaultValue : () => false ),
2024-10-24 15:56:39 +02:00
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" ),
2024-12-05 12:20:11 +01:00
new Option < string? >( "--webservice-password" , description : "The password for the webserver, not set, or set to \"random\", a random value is used." , getDefaultValue : () => null ),
2024-10-24 15:56:39 +02:00
new Option < string? >( "--settings-encryption-key" , description : "The encryption key for the database settings" , getDefaultValue : () => null ),
2024-12-20 16:09:42 +01:00
new Option < string >( "--windows-eventlog" , description : "The Windows event log to use as source." , getDefaultValue : () => "" ),
2024-10-24 15:56:39 +02:00
new Option < bool >( "--disable-db-encryption" , description : "Disable database encryption" , getDefaultValue : () => false ),
new Option < bool >( "--webservice-reset-jwt-config" , description : "Reset the JWT configuration" , getDefaultValue : () => true ),
new Option < string >( "--webservice-allowed-hostnames" , description : "The allowed hostnames for the webserver" , getDefaultValue : () => "127.0.0.1" ),
new Option < bool >( "--webservice-api-only" , description : "Only allow API access to the webserver" , getDefaultValue : () => true ),
new Option < bool >( "--webservice-disable-signin-tokens" , description : "Disable signin tokens for the webserver" , getDefaultValue : () => true ),
new Option < bool >( "--ping-pong-keepalive" , description : "Enable ping-pong keepalive" , getDefaultValue : () => false ),
2024-11-12 16:39:12 +01:00
new Option < bool >( "--disable-pre-shared-key" , description : "Disable the pre-shared key that prevents outside access to the webserver" , getDefaultValue : () => false ),
2024-12-05 12:20:11 +01:00
new Option < bool >( "--keep-webservice-password" , description : "Disables the random password assigned to the webserver on startup if no password is provided" , getDefaultValue : () => false ),
2024-10-24 15:56:39 +02:00
new Option < string? >( "--secret-provider" , description : "The secret provider to use" , getDefaultValue : () => null ),
new Option < SecretProviderHelper . CachingLevel >( "--secret-provider-cache" , description : "The secret provider cache level" , getDefaultValue : () => SecretProviderHelper . CachingLevel . None ),
2024-10-26 08:37:26 +02:00
new Option < string >( "--secret-provider-pattern" , description : "The secret provider pattern" , getDefaultValue : () => SecretProviderHelper . DEFAULT_PATTERN ),
2025-01-28 13:01:35 +01:00
new Option < bool >( $"--{DataFolderManager.PORTABLE_MODE_OPTION}" , description : "Use portable mode for locating the database and storing configuration" , getDefaultValue : () => DataFolderManager . PORTABLE_MODE ),
new Option < DirectoryInfo ?>( $"--{DataFolderManager.SERVER_DATAFOLDER_OPTION}" , description : "The datafolder to use for locating the database and storing configuration" , getDefaultValue : () => new DirectoryInfo ( DataFolderManager . DATAFOLDER )),
2024-09-18 11:07:59 +02:00
};
2024-10-24 15:56:39 +02:00
runcmd . Handler = CommandHandler . Create < CommandLineArguments >( RunAgent );
2024-09-18 11:07:59 +02:00
2024-10-24 15:56:39 +02:00
var clearCommand = new Command ( "clear" , "Clears the agent settings" )
2024-09-18 11:07:59 +02:00
{
2024-10-24 15:56:39 +02:00
new Option < FileInfo >( "--agent-settings-file" , description : "The file to use for the agent settings" , getDefaultValue : () => new FileInfo ( Settings . DefaultSettingsFile )),
};
clearCommand . Handler = CommandHandler . Create < FileInfo >( ClearAgentSettings );
2024-12-03 08:30:47 +01:00
var registerCommand = new Command ( "register" , "Registers the agent and exits" )
{
new Argument < string >( "agent-registration-url" , description : "The server URL to connect to" , getDefaultValue : () => RegisterForRemote . DefaultRegisterationUrl ) { Arity = ArgumentArity . ZeroOrOne },
new Option < FileInfo >( "--agent-settings-file" , description : "The file to use for the agent settings" , getDefaultValue : () => new FileInfo ( Settings . DefaultSettingsFile )),
new Option < string? >( "--agent-settings-file-passphrase" , description : "The passphrase for the agent settings file" , getDefaultValue : () => null ),
new Option < string? >( "--secret-provider" , description : "The secret provider to use" , getDefaultValue : () => null ),
new Option < string >( "--secret-provider-pattern" , description : "The secret provider pattern" , getDefaultValue : () => SecretProviderHelper . DEFAULT_PATTERN ),
};
registerCommand . Handler = CommandHandler . Create < string , FileInfo , string? , string? , string >( RegisterAgent );
2024-10-24 15:56:39 +02:00
var rootcmd = new RootCommand ( "Duplicati Agent" )
{
runcmd ,
2024-12-03 08:30:47 +01:00
clearCommand ,
registerCommand
2024-10-24 15:56:39 +02:00
};
// rootcmd.Handler = CommandHandler.Create<CommandLineArguments>(RunAgent);
2024-09-18 11:07:59 +02:00
2024-10-24 15:56:39 +02:00
return new CommandLineBuilder ( rootcmd )
. UseDefaults ()
. UseExceptionHandler (( ex , context ) =>
{
if ( ex is UserInformationException userInformationException )
{
Console . WriteLine ( "ErrorID: {0}" , userInformationException . HelpID );
Console . WriteLine ( "Message: {0}" , userInformationException . Message );
context . ExitCode = 2 ;
}
else
{
Console . WriteLine ( "Exception: " + ex );
context . ExitCode = 1 ;
}
})
2025-01-17 14:00:14 -03:00
. UseAdditionalHelpAliases ()
2024-10-24 15:56:39 +02:00
. Build ()
. InvokeAsync ( args );
}
/// <summary>
/// Clears any local agent settings
/// </summary>
/// <param name="agentSettingsFile">The file to clear</param>
/// <returns>The exit code</returns>
private static Task < int > ClearAgentSettings ( FileInfo agentSettingsFile )
{
if ( File . Exists ( agentSettingsFile . FullName ))
File . Delete ( agentSettingsFile . FullName );
return Task . FromResult ( 0 );
2024-09-18 11:07:59 +02:00
}
2024-12-03 08:30:47 +01:00
/// <summary>
/// Registers the agent with the remote server
/// </summary>
/// <param name="agentRegistrationUrl">The URL to register the agent with</param>
/// <param name="agentSettingsFile">The file to save the settings to</param>
/// <param name="agentSettingsFilePassphrase">The passphrase for the settings file</param>
/// <param name="secretProvider">The secret provider to use</param>
/// <param name="secretProviderPattern">The secret provider pattern</param>
/// <returns></returns>
private static Task < int > RegisterAgent ( string agentRegistrationUrl , FileInfo agentSettingsFile , string? agentSettingsFilePassphrase , string? secretProvider , string secretProviderPattern )
=> RunAgent ( new CommandLineArguments (
AgentRegistrationUrl : string . IsNullOrWhiteSpace ( agentRegistrationUrl )
? RegisterForRemote . DefaultRegisterationUrl
: agentRegistrationUrl ,
AgentSettingsFile : agentSettingsFile ,
AgentSettingsFilePassphrase : agentSettingsFilePassphrase ,
AgentRegisterOnly : true ,
WebserviceListenInterface : "loopback" ,
WebservicePort : "8210" ,
WebservicePassword : null !,
SettingsEncryptionKey : null ,
2024-12-20 16:09:42 +01:00
WindowsEventLog : "" ,
2024-12-03 08:30:47 +01:00
DisableDbEncryption : false ,
WebserviceResetJwtConfig : false ,
WebserviceAllowedHostnames : "" ,
WebserviceApiOnly : true ,
WebserviceDisableSigninTokens : true ,
PingPongKeepalive : false ,
DisablePreSharedKey : false ,
2024-12-05 12:20:11 +01:00
KeepWebservicePassword : false ,
2024-12-03 08:30:47 +01:00
SecretProvider : secretProvider ,
SecretProviderCache : SecretProviderHelper . CachingLevel . None ,
SecretProviderPattern : secretProviderPattern
));
2024-09-18 11:07:59 +02:00
/// <summary>
/// Runs the agent
/// </summary>
/// <param name="agentConfig">The configuration from the commandline</param>
/// <returns>The exit code</returns>
2024-10-24 15:56:39 +02:00
private static async Task < int > RunAgent ( CommandLineArguments agentConfig )
2024-09-18 11:07:59 +02:00
{
2024-10-24 15:56:39 +02:00
// Read the environment variable for the encryption key
if ( string . IsNullOrWhiteSpace ( agentConfig . SettingsEncryptionKey ))
agentConfig = agentConfig with { SettingsEncryptionKey = Environment . GetEnvironmentVariable ( EncryptedFieldHelper . ENVIROMENT_VARIABLE_NAME ) };
// Apply the secret provider, if present
if (! string . IsNullOrWhiteSpace ( agentConfig . SecretProvider ))
{
// We can set any string properties on the agent configuration
var agentProps = agentConfig . GetType (). GetProperties ()
. Where ( x => x . CanRead && x . CanWrite )
. Where ( x => ! x . Name . StartsWith ( nameof ( CommandLineArguments . SecretProvider )))
. Where ( x => x . PropertyType == typeof ( string ) || Nullable . GetUnderlyingType ( x . PropertyType ) == typeof ( string ))
. ToDictionary ( x => x . Name , x => x );
// Grab potential options from the agent configuration
var options = agentProps . Select ( x => ( x . Key , Value : x . Value . GetValue ( agentConfig )?. ToString ()))
. Where ( x => x . Value != null )
2024-10-24 16:52:09 +02:00
. ToDictionary ( x => x . Key , x => x . Value );
2024-10-24 15:56:39 +02:00
// Patch with the secret provider options
options [ "secret-provider" ] = agentConfig . SecretProvider ;
options [ "secret-provider-cache" ] = agentConfig . SecretProviderCache . ToString ();
options [ "secret-provider-pattern" ] = agentConfig . SecretProviderPattern ;
2024-11-04 15:57:56 +01:00
FIXMEGlobal . SecretProvider = await SecretProviderHelper . ApplySecretProviderAsync ([], [], options , TempFolder . SystemTempPath , null , CancellationToken . None );
2024-10-24 15:56:39 +02:00
// Apply the secret provider to the agent configuration
foreach ( var prop in agentProps )
if ( options . TryGetValue ( prop . Key , out string? value ))
prop . Value . SetValue ( agentConfig , value );
}
2024-12-05 12:20:11 +01:00
if ( agentConfig . KeepWebservicePassword )
{
// Check for conflicting options, require no password
if (! string . IsNullOrWhiteSpace ( agentConfig . WebservicePassword ))
throw new UserInformationException ( "Cannot use --keep-webservice-password with a provided password" , "KeepWebservicePasswordWithPassword" );
}
else
{
// Prevent access to the webserver interface from anything but the agent
if ( string . Equals ( "random" , agentConfig . WebservicePassword , StringComparison . OrdinalIgnoreCase ) || string . IsNullOrWhiteSpace ( agentConfig . WebservicePassword ))
agentConfig = agentConfig with { WebservicePassword = System . Security . Cryptography . RandomNumberGenerator . GetHexString ( 128 ) };
}
2024-09-18 11:07:59 +02:00
2024-09-27 11:19:10 +02:00
// Set the pre-shared key for the agent
if (! agentConfig . DisablePreSharedKey )
WebserverCore . Middlewares . PreSharedKeyFilter . PreSharedKey = PreSharedKey = RandomNumberGenerator . GetHexString ( 128 );
2024-09-18 11:07:59 +02:00
using var cts = new CancellationTokenSource ();
var target = new ControllerMultiLogTarget ( new ConsoleLogDestination (), LogMessageType . Information , null );
using ( Log . StartScope ( target ))
{
2024-12-20 16:09:42 +01:00
if ( OperatingSystem . IsWindows () && ! string . IsNullOrWhiteSpace ( agentConfig . WindowsEventLog ))
2024-09-18 11:07:59 +02:00
{
if (! WindowsEventLogSource . SourceExists ( agentConfig . WindowsEventLog ))
{
2024-12-20 16:09:42 +01:00
Console . WriteLine ( $"The Windows event log {agentConfig.WindowsEventLog} does not exist, creating it" );
2024-09-18 11:07:59 +02:00
try { WindowsEventLogSource . CreateEventSource ( agentConfig . WindowsEventLog ); }
2024-12-20 16:09:42 +01:00
catch ( Exception ex ) { Console . WriteLine ( $"Failed to create the Windows event log {agentConfig.WindowsEventLog}: {ex.Message}" ); }
2024-09-18 11:07:59 +02:00
}
if ( WindowsEventLogSource . SourceExists ( agentConfig . WindowsEventLog ))
target . AddTarget ( new WindowsEventLogSource ( agentConfig . WindowsEventLog ), LogMessageType . Information , null );
}
Log . WriteMessage ( LogMessageType . Information , LogTag , "AgentStarting" , "Starting agent" );
2024-10-24 15:56:39 +02:00
var settings = await Register ( agentConfig . AgentRegistrationUrl , agentConfig . AgentSettingsFile . FullName , agentConfig . AgentSettingsFilePassphrase , cts . Token );
2024-09-18 11:07:59 +02:00
2024-10-25 15:03:01 +02:00
if ( agentConfig . AgentRegisterOnly )
2024-12-03 08:30:47 +01:00
{
Log . WriteMessage ( LogMessageType . Information , LogTag , "ClientRegistered" , "Client registered, exiting" );
2024-10-25 15:03:01 +02:00
return 0 ;
2024-12-03 08:30:47 +01:00
}
2024-10-25 15:03:01 +02:00
2024-09-18 11:07:59 +02:00
var t = await Task . WhenAny (
2024-10-24 15:56:39 +02:00
Task . Run (() => StartLocalServer ( agentConfig , settings , cts . Token )),
2024-09-18 11:07:59 +02:00
KeepRemoteConnection . Start (
settings . ServerUrl ,
settings . JWT ,
2024-09-23 11:45:12 +02:00
settings . CertificateUrl ,
2024-09-20 09:42:21 +02:00
settings . ServerCertificates ,
2024-09-18 11:07:59 +02:00
cts . Token ,
2024-10-24 15:56:39 +02:00
c => ReKey ( agentConfig , c ),
2024-09-18 11:07:59 +02:00
( m ) => OnMessage ( m , agentConfig )
)
);
await t ;
return 0 ;
}
}
private static async Task OnMessage ( KeepRemoteConnection . CommandMessage message , CommandLineArguments agentConfig )
{
2024-12-20 16:09:42 +01:00
Log . WriteMessage ( LogMessageType . Verbose , LogTag , "OnMessage" , "Received message: {0}" , message );
2024-09-18 11:07:59 +02:00
2024-09-28 16:43:39 +02:00
var provider = FIXMEGlobal . Provider . GetRequiredService < IJWTTokenProvider >();
var token = provider . CreateAccessToken ( "agent" , provider . TemporaryFamilyId , TimeSpan . FromMinutes ( 2 ));
2024-09-18 11:07:59 +02:00
using var httpClient = FIXMEGlobal . Provider . GetRequiredService < IHttpClientFactory >(). CreateClient ();
httpClient . DefaultRequestHeaders . Authorization = new System . Net . Http . Headers . AuthenticationHeaderValue ( "Bearer" , token );
2024-10-24 15:56:39 +02:00
httpClient . BaseAddress = new System . Uri ( $"http://127.0.0.1:{agentConfig.WebservicePort}" );
2024-09-18 11:07:59 +02:00
2024-09-27 11:19:10 +02:00
if (! string . IsNullOrWhiteSpace ( WebserverCore . Middlewares . PreSharedKeyFilter . PreSharedKey ))
httpClient . DefaultRequestHeaders . Add ( WebserverCore . Middlewares . PreSharedKeyFilter . HeaderName , PreSharedKey );
2024-09-18 11:07:59 +02:00
await message . Handle ( httpClient );
}
2024-10-24 15:56:39 +02:00
private static Task ReKey ( CommandLineArguments agentConfig , ClaimedClientData keydata )
2024-09-18 11:07:59 +02:00
{
2024-12-20 16:09:42 +01:00
Log . WriteMessage ( LogMessageType . Verbose , LogTag , "ReKey" , "Rekeying the settings" );
2024-10-24 15:56:39 +02:00
var settings = Settings . Load ( agentConfig . AgentSettingsFile . FullName , agentConfig . AgentSettingsFilePassphrase );
2024-09-18 11:07:59 +02:00
if (! string . IsNullOrWhiteSpace ( keydata . JWT ) && settings . JWT != keydata . JWT )
settings = settings with { JWT = keydata . JWT };
2024-09-23 23:18:36 +02:00
if ( keydata . ServerCertificates != null && keydata . ServerCertificates . Any ())
settings = settings with { ServerCertificates = keydata . ServerCertificates };
2024-09-18 11:07:59 +02:00
2024-10-24 15:56:39 +02:00
// Only allow re-keying if the settings encryption key is not set
if (! FIXMEGlobal . SettingsEncryptionKeyProvidedExternally )
2024-09-18 11:07:59 +02:00
{
2024-10-24 15:56:39 +02:00
if (! string . IsNullOrWhiteSpace ( keydata . LocalEncryptionKey ) && settings . SettingsEncryptionKey != keydata . LocalEncryptionKey )
{
2024-12-20 16:09:42 +01:00
// Log.WriteMessage(LogMessageType.Verbose, LogTag, "ReKey", "Changing the local settings encryption key");
2024-10-24 15:56:39 +02:00
// TODO: Implement changing the database encryption key
// FIXMEGlobal.Provider.GetRequiredService<Connection>().ChangeDbKey(keydata.LocalEncryptionKey);
// settings = settings with { SettingsEncryptionKey = keydata.LocalEncryptionKey };
}
2024-09-18 11:07:59 +02:00
}
settings . Save ();
return Task . CompletedTask ;
}
2024-10-24 15:56:39 +02:00
/// <summary>
/// Runs the server, restarting on crashes
/// </summary>
/// <param name="args">The commandline arguments passed to the server</param>
/// <param name="cancellationToken">The cancellation token to use for the process</param>
/// <returns>An awaitable task</returns>
2024-09-18 11:07:59 +02:00
private static async Task RunServer ( string [] args , CancellationToken cancellationToken )
{
cancellationToken . Register (() =>
{
Server . Program . ApplicationExitEvent . Set ();
});
var lastRestart = DateTime . Now ;
while (! cancellationToken . IsCancellationRequested )
{
try
{
lastRestart = DateTime . Now ;
Server . Program . Main ( args );
}
catch ( Exception ex )
{
if (! cancellationToken . IsCancellationRequested )
{
var nextRestart = lastRestart . AddSeconds ( 30 );
var waitTime = nextRestart - DateTime . Now ;
if ( waitTime . TotalSeconds < 0 )
waitTime = TimeSpan . FromSeconds ( 0 );
Log . WriteMessage ( LogMessageType . Error , LogTag , "ServerCrash" , ex , "Server crashed, restarting in {0} seconds" , waitTime . TotalSeconds );
if ( waitTime . TotalSeconds > 0 )
await Task . Delay ( waitTime );
}
}
}
}
/// <summary>
/// Registers the machine with the remote server
/// </summary>
/// <param name="registrationUrl">The URL to register the machine with</param>
/// <param name="settingsFile">The file to save the settings to</param>
2024-10-24 15:56:39 +02:00
/// <param name="settingsPasshrase">The passphrase for the settings file</param>
2024-09-18 11:07:59 +02:00
/// <param name="cancellationToken">The cancellation token to use for the process</param>
/// <returns>The settings for the machine</returns>
2024-10-24 15:56:39 +02:00
private static async Task < Settings > Register ( string registrationUrl , string settingsFile , string? settingsPasshrase , CancellationToken cancellationToken )
2024-09-18 11:07:59 +02:00
{
2024-10-24 15:56:39 +02:00
var settings = Settings . Load ( settingsFile , settingsPasshrase );
2024-09-18 11:07:59 +02:00
if ( string . IsNullOrWhiteSpace ( settings . JWT ))
{
Log . WriteMessage ( LogMessageType . Information , LogTag , "ClientNoJWT" , "No JWT found in settings, starting in registration mode" );
using ( var registration = new RegisterForRemote ( registrationUrl , null , cancellationToken ))
{
var registerClientData = await registration . Register ();
2024-10-16 16:28:13 +02:00
if ( registerClientData . RegistrationData != null )
2024-10-17 14:25:41 +02:00
{
2024-10-16 16:28:13 +02:00
Log . WriteMessage ( LogMessageType . Information , LogTag , "ClientRegistered" , $"Machine registered, claim it by visiting: {registerClientData.RegistrationData.ClaimLink}" );
2024-10-17 14:25:41 +02:00
try
{
Library . Utility . UrlUtility . OpenURL ( registerClientData . RegistrationData . ClaimLink );
}
catch ( Exception ex )
{
Log . WriteMessage ( LogMessageType . Warning , LogTag , "ClientClaimLink" , ex , "Failed to open the claim link in system browser" );
}
}
2024-09-18 11:07:59 +02:00
var claimedClientData = await registration . Claim ();
Log . WriteMessage ( LogMessageType . Information , LogTag , "ClientClaimed" , "Machine claimed, saving JWT" );
settings = settings with
{
JWT = claimedClientData . JWT ,
ServerUrl = claimedClientData . ServerUrl ,
SettingsEncryptionKey = claimedClientData . LocalEncryptionKey ,
2024-10-24 15:56:39 +02:00
ServerCertificates = claimedClientData . ServerCertificates ,
CertificateUrl = claimedClientData . CertificateUrl
2024-09-18 11:07:59 +02:00
};
settings . Save ();
}
}
return settings ;
}
/// <summary>
/// Starts the local webserver in locked down mode, restarting on crashes
/// </summary>
/// <param name="agentConfig">The agent configuration</param>
/// <param name="settings">The settings for the agent</param>
/// <param name="cancellationToken">The cancellation token that stops the server</param>
/// <returns>An awaitable task</returns>
2024-10-24 15:56:39 +02:00
private static async Task StartLocalServer ( CommandLineArguments agentConfig , Settings settings , CancellationToken cancellationToken )
2024-09-18 11:07:59 +02:00
{
// TODO: Look into pipes for Kestrel to prevent network access
2024-10-24 15:56:39 +02:00
string? settingsEncryptionKey = null ;
// Prefer the local key for the settings encryption
if (! string . IsNullOrWhiteSpace ( agentConfig . SettingsEncryptionKey ))
settingsEncryptionKey = agentConfig . SettingsEncryptionKey ;
// Otherwise use server provided key
else if (! string . IsNullOrWhiteSpace ( settings . SettingsEncryptionKey ))
settingsEncryptionKey = settings . SettingsEncryptionKey ;
2024-12-05 12:20:11 +01:00
// Helper method to support empty arguments
static string EncodeOption ( string option , string? value )
{
if ( string . IsNullOrWhiteSpace ( value ))
return "" ;
return $"{option}={value}" ;
}
2024-10-24 15:56:39 +02:00
// Lock down the instance, reset tokens and password,
// and forward relevant settings from agent to the webserver
var args = new [] {
2024-12-05 12:20:11 +01:00
EncodeOption ( "--webservice-listen-interface" , agentConfig . WebserviceListenInterface ),
EncodeOption ( "--webservice-password" , agentConfig . WebservicePassword ),
EncodeOption ( "--webservice-port" , agentConfig . WebservicePort ),
EncodeOption ( "--webservice-reset-jwt-config" , agentConfig . WebserviceResetJwtConfig . ToString ()),
EncodeOption ( "--webservice-allowed-hostnames" , agentConfig . WebserviceAllowedHostnames ),
EncodeOption ( "--webservice-api-only" , agentConfig . WebserviceApiOnly . ToString ()),
EncodeOption ( "--webservice-disable-signin-tokens" , agentConfig . WebserviceDisableSigninTokens . ToString ()),
EncodeOption ( "--disable-db-encryption" , agentConfig . DisableDbEncryption . ToString ()),
EncodeOption ( "--settings-encryption-key" , settingsEncryptionKey ),
2024-10-24 15:56:39 +02:00
}
. Where ( x => ! string . IsNullOrWhiteSpace ( x ))
. ToArray ();
// Set the global origin
FIXMEGlobal . Origin = "Agent" ;
2024-09-18 11:07:59 +02:00
// Start the server
await RunServer ( args , cancellationToken );
}
}