2025-02-08 17:31:49 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-06-27 11:19:24 -03:00
//
// 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
2025-02-08 17:31:49 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-06-27 11:19:24 -03:00
//
// The above copyright notice and this permission notice shall be included in
2025-02-08 17:31:49 +01:00
// all copies or substantial portions of the Software.
2025-06-27 11:19:24 -03:00
//
// 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-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2025-07-08 13:03:33 +02:00
#nullable enable
2024-02-28 15:45:30 +01:00
using System ;
2013-02-12 21:43:14 +00:00
using System.Collections.Generic ;
using System.Linq ;
2017-05-03 00:03:16 +02:00
using System.Net ;
2024-06-19 13:54:10 +02:00
using System.Net.Http ;
using System.Net.Http.Headers ;
2024-10-29 13:28:44 +01:00
using System.Net.Security ;
2013-02-12 21:43:14 +00:00
using System.Text ;
2024-06-19 13:54:10 +02:00
using System.Text.Json ;
using System.Text.Json.Serialization ;
using System.Threading ;
using System.Threading.Tasks ;
2025-03-25 10:54:10 +01:00
using CoCoL ;
2019-04-27 16:35:42 -07:00
using Duplicati.Library.Common.IO ;
2013-02-12 21:43:14 +00:00
using Duplicati.Server.Serialization ;
2013-11-23 14:26:36 +01:00
using Duplicati.Server.Serialization.Interface ;
2024-06-19 13:54:10 +02:00
using Duplicati.WebserverCore ;
using Duplicati.WebserverCore.Abstractions ;
using Duplicati.WebserverCore.Middlewares ;
using Microsoft.Extensions.DependencyInjection ;
2013-02-12 21:43:14 +00:00
namespace Duplicati.GUI.TrayIcon
{
public class HttpServerConnection : IDisposable
{
2024-06-19 13:54:10 +02:00
private static readonly string LOGTAG = Library . Logging . Log . LogTagFromType < HttpServerConnection >();
private const string LONGPOLL_TIMEOUT = "5m" ;
2024-10-29 13:28:44 +01:00
private readonly HttpClient HTTPCLIENT ;
2025-06-25 15:24:04 -03:00
2024-06-19 13:54:10 +02:00
private record ServerStatusImpl (
LiveControlState ProgramState ,
SuggestedStatusIcon SuggestedStatusIcon ,
long LastEventID ,
long LastDataUpdateID ,
2024-08-13 22:37:36 +02:00
long LastNotificationUpdateID
2024-06-19 13:54:10 +02:00
) : IServerStatus ;
2024-06-20 22:22:49 +02:00
private record NotificationImpl (
long ID ,
Server . Serialization . NotificationType Type ,
string Title ,
string Message ,
string Exception ,
string BackupID ,
string Action ,
DateTime Timestamp ,
string LogEntryID ,
string MessageID ,
string MessageLogTag
) : INotification ;
2024-06-19 13:54:10 +02:00
private readonly JsonSerializerOptions serializerOptions = new ()
{
PropertyNamingPolicy = null ,
2025-06-25 15:24:04 -03:00
Converters =
{
2024-06-19 13:54:10 +02:00
new JsonStringEnumConverter (),
new DayOfWeekStringEnumConverter ()
}
};
private const string STATUS_WINDOW = "index.html" ;
private const string SIGNIN_WINDOW = "signin.html" ;
2025-07-08 13:03:33 +02:00
private record BackgroundRequest ( string Method , string Endpoint , string? Body , TimeSpan ? Timeout = null );
2015-07-30 14:31:27 +02:00
2018-05-23 21:18:01 -07:00
private readonly string m_apiUri ;
private readonly string m_baseUri ;
2014-06-27 13:23:05 +02:00
private string m_password ;
2025-07-08 13:03:33 +02:00
private string? m_accesstoken ;
2025-02-08 17:31:49 +01:00
private bool m_isTryingWithPassword ;
2014-08-04 20:38:43 +02:00
2025-07-08 13:03:33 +02:00
public Func < IServerStatus , Task >? OnStatusUpdated ;
public Action ? ConnectionClosed ;
2025-06-25 15:24:04 -03:00
private int _connectionClosedInvoked ;
2014-08-04 22:16:15 +02:00
public long m_lastNotificationId = - 1 ;
public DateTime m_firstNotificationTime ;
2025-06-25 15:24:04 -03:00
2014-08-04 22:16:15 +02:00
public delegate void NewNotificationDelegate ( INotification notification );
2025-06-25 15:24:04 -03:00
2025-07-08 13:03:33 +02:00
public event NewNotificationDelegate ? OnNotification ;
2014-08-04 22:16:15 +02:00
2024-06-19 13:54:10 +02:00
private long m_lastEventId = 0 ;
2018-06-15 14:35:35 +02:00
private long m_lastDataUpdateId = - 1 ;
private bool m_disableTrayIconLogin ;
2025-06-25 15:24:04 -03:00
private volatile IServerStatus m_status =
new ServerStatusImpl ( LiveControlState . Paused , SuggestedStatusIcon . Disconnected , 0 , 0 , 0 );
2013-02-12 21:43:14 +00:00
2025-03-25 10:54:10 +01:00
private Task m_requestHandlerTask ;
private Task m_pollThread ;
private readonly CancellationTokenSource m_stopToken = new CancellationTokenSource ();
2013-02-12 21:43:14 +00:00
2017-06-28 21:39:45 +02:00
private readonly Dictionary < string , string > m_options ;
2018-02-25 13:54:20 +01:00
private readonly Program . PasswordSource m_passwordSource ;
2013-02-12 21:43:14 +00:00
2025-06-25 15:24:04 -03:00
public IServerStatus Status
{
get { return m_status ; }
}
private readonly IChannel < BackgroundRequest > m_workQueue =
Channel . Create < BackgroundRequest >( name : "TrayIconRequestQueue" );
2013-02-12 21:43:14 +00:00
2025-08-07 11:50:01 +02:00
private readonly CancellationToken _applicationExitEvent ;
2013-02-12 21:43:14 +00:00
2025-07-08 13:03:33 +02:00
public HttpServerConnection ( IApplicationSettings ? applicationSettings , System . Uri server , string password ,
2025-06-25 15:24:04 -03:00
Program . PasswordSource passwordSource , bool disableTrayIconLogin , string acceptedHostCertificate ,
Dictionary < string , string > options )
2013-02-12 21:43:14 +00:00
{
2025-08-07 11:50:01 +02:00
_applicationExitEvent = applicationSettings ?. ApplicationExit
?? CancellationToken . None ;
2025-06-25 15:24:04 -03:00
2018-10-27 12:17:07 +02:00
m_baseUri = Util . AppendDirSeparator ( server . ToString (), "/" );
2015-07-30 14:31:27 +02:00
m_apiUri = m_baseUri + "api/v1" ;
2018-06-15 14:35:35 +02:00
m_disableTrayIconLogin = disableTrayIconLogin ;
2014-08-04 22:16:15 +02:00
m_firstNotificationTime = DateTime . Now ;
2014-06-27 13:23:05 +02:00
m_password = password ;
2017-06-28 21:39:45 +02:00
m_options = options ;
2018-02-25 13:54:20 +01:00
m_passwordSource = passwordSource ;
2014-06-27 13:23:05 +02:00
2024-10-29 14:38:23 +01:00
var acceptedCertificates = new HashSet < string >( StringComparer . OrdinalIgnoreCase );
if (! string . IsNullOrWhiteSpace ( acceptedHostCertificate ))
2025-08-07 11:24:10 +02:00
acceptedCertificates . UnionWith ( acceptedHostCertificate . Split ( new char [] { ',' , ';' },
2025-06-25 15:24:04 -03:00
StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ));
2024-10-29 14:38:23 +01:00
2024-10-29 13:28:44 +01:00
HTTPCLIENT = new ( new HttpClientHandler
{
2024-10-29 14:38:23 +01:00
ServerCertificateCustomValidationCallback = acceptedCertificates switch
2024-10-29 13:28:44 +01:00
{
2025-07-08 13:03:33 +02:00
{ Count : 0 } or null => null ,
2025-06-25 15:24:04 -03:00
{ } when acceptedCertificates . Contains ( "*" ) => HttpClientHandler
. DangerousAcceptAnyServerCertificateValidator ,
2025-07-08 13:03:33 +02:00
2024-10-29 13:28:44 +01:00
_ => ( sender , cert , chain , sslPolicyErrors ) =>
{
if ( sslPolicyErrors == SslPolicyErrors . None )
return true ;
2024-10-29 14:38:23 +01:00
if ( cert == null )
return false ;
var certHash = cert . GetCertHashString ();
return acceptedCertificates . Contains ( certHash );
2024-10-29 13:28:44 +01:00
}
}
})
{
// Max time a request can be pending, actual requests can set a lower limit
Timeout = Library . Utility . Timeparser . ParseTimeSpan ( LONGPOLL_TIMEOUT ) + TimeSpan . FromSeconds ( 10 ),
2025-06-25 15:24:04 -03:00
DefaultRequestHeaders =
{
UserAgent =
{
new ProductInfoHeaderValue ( "Duplicati-TrayIcon-Monitor" ,
2025-07-08 13:03:33 +02:00
System . Reflection . Assembly . GetExecutingAssembly (). GetName (). Version ?. ToString () ?? "0.0.0.0" )
2025-06-25 15:24:04 -03:00
}
2024-10-29 13:28:44 +01:00
}
};
2025-03-25 10:54:10 +01:00
m_requestHandlerTask = ThreadRunner ();
m_pollThread = LongPollRunner ();
2013-02-12 21:43:14 +00:00
2025-03-25 10:54:10 +01:00
m_requestHandlerTask . ContinueWith ( t =>
{
if ( t . IsFaulted )
2025-06-25 15:24:04 -03:00
Library . Logging . Log . WriteWarningMessage ( LOGTAG , "TrayIconRequestError" , t . Exception ,
"Crashed request handler" );
if ( Interlocked . CompareExchange ( ref _connectionClosedInvoked , 1 , 0 ) == 0 )
ConnectionClosed ?. Invoke ();
2025-03-25 10:54:10 +01:00
});
2013-02-12 21:43:14 +00:00
2025-03-25 10:54:10 +01:00
m_pollThread . ContinueWith ( t =>
{
if ( t . IsFaulted )
2025-06-25 15:24:04 -03:00
Library . Logging . Log . WriteWarningMessage ( LOGTAG , "TrayIconRequestError" , t . Exception ,
"Crashed poll thread" );
if ( Interlocked . CompareExchange ( ref _connectionClosedInvoked , 1 , 0 ) == 0 )
ConnectionClosed ?. Invoke ();
2025-03-25 10:54:10 +01:00
});
2013-02-12 21:43:14 +00:00
}
2025-02-08 17:31:49 +01:00
public Task UpdateStatus ()
=> UpdateStatusAsync ( false );
private async Task UpdateStatusAsync ( bool longpoll )
2013-02-12 21:43:14 +00:00
{
2024-06-19 13:54:10 +02:00
var query = longpoll ? $"?longpoll=true&lastEventId={m_lastEventId}&duration={LONGPOLL_TIMEOUT}" : "" ;
2025-06-25 15:24:04 -03:00
m_status = await PerformRequestAsync < ServerStatusImpl >( "GET" , $"/serverstate{query}" , null ,
longpoll ? Library . Utility . Timeparser . ParseTimeSpan ( LONGPOLL_TIMEOUT ) : null );
2024-06-19 13:54:10 +02:00
m_lastEventId = m_status . LastEventID ;
2013-02-12 21:43:14 +00:00
2014-08-04 20:38:43 +02:00
if ( OnStatusUpdated != null )
2025-03-25 10:54:10 +01:00
await OnStatusUpdated ( m_status ). ConfigureAwait ( false );
2014-08-04 22:16:15 +02:00
if ( m_lastNotificationId != m_status . LastNotificationUpdateID )
{
m_lastNotificationId = m_status . LastNotificationUpdateID ;
2025-02-08 17:31:49 +01:00
await UpdateNotificationsAsync (). ConfigureAwait ( false );
2014-08-04 22:16:15 +02:00
}
2018-06-15 14:35:35 +02:00
if ( m_lastDataUpdateId != m_status . LastDataUpdateID )
{
m_lastDataUpdateId = m_status . LastDataUpdateID ;
2025-02-08 17:31:49 +01:00
await UpdateApplicationSettingsAsync (). ConfigureAwait ( false );
2018-06-15 14:35:35 +02:00
}
2014-08-04 22:16:15 +02:00
}
2025-02-08 17:31:49 +01:00
private async Task UpdateNotificationsAsync ()
2014-08-04 22:16:15 +02:00
{
2025-06-25 15:24:04 -03:00
var notifications = await PerformRequestAsync < NotificationImpl []>( "GET" , "/notifications" , null , null )
. ConfigureAwait ( false );
2014-08-04 22:16:15 +02:00
if ( notifications != null )
{
2024-06-19 13:54:10 +02:00
foreach ( var n in notifications . Where ( x => x . Timestamp > m_firstNotificationTime ))
2014-08-04 22:16:15 +02:00
if ( OnNotification != null )
OnNotification ( n );
if ( notifications . Any ())
m_firstNotificationTime = notifications . Select ( x => x . Timestamp ). Max ();
}
2013-02-12 21:43:14 +00:00
}
2025-02-08 17:31:49 +01:00
private async Task UpdateApplicationSettingsAsync ()
2018-06-15 14:35:35 +02:00
{
2025-06-25 15:24:04 -03:00
var settings = await PerformRequestAsync < Dictionary < string , string >>( "GET" , "/serversettings" , null , null )
. ConfigureAwait ( false );
2018-06-15 14:35:35 +02:00
if ( settings != null && settings . TryGetValue ( "disable-tray-icon-login" , out var str ))
m_disableTrayIconLogin = Library . Utility . Utility . ParseBool ( str , false );
}
2025-03-25 10:54:10 +01:00
private async Task LongPollRunner ()
2013-02-12 21:43:14 +00:00
{
2024-06-20 14:22:49 +02:00
var started = DateTime . Now ;
var errorCount = 0 ;
2025-03-25 10:54:10 +01:00
var hasConnected = false ;
2024-06-20 14:22:49 +02:00
2025-08-07 11:50:01 +02:00
while (! m_stopToken . IsCancellationRequested && ! _applicationExitEvent . IsCancellationRequested )
2013-02-12 21:43:14 +00:00
{
try
{
2024-12-19 22:37:44 +01:00
var waitTime = TimeSpan . FromSeconds ( Math . Min ( 10 , errorCount * 2 )) - ( DateTime . Now - started );
if ( waitTime . TotalSeconds > 0 )
2025-06-25 15:24:04 -03:00
{
2025-08-07 11:50:01 +02:00
// Wait for the specified time or until stop or exit is requested
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( m_stopToken . Token , _applicationExitEvent );
await Task . Delay ( waitTime , cts . Token ). ConfigureAwait ( false );
2025-06-25 15:24:04 -03:00
}
2024-06-20 14:22:49 +02:00
started = DateTime . Now ;
2025-06-25 15:24:04 -03:00
await UpdateStatusAsync ( true ). ConfigureAwait ( false );
2024-06-20 14:22:49 +02:00
errorCount = 0 ;
2025-03-25 10:54:10 +01:00
hasConnected = true ;
}
2025-08-07 11:50:01 +02:00
catch ( Exception ex ) when ( ex . IsRetiredException () || m_stopToken . IsCancellationRequested || _applicationExitEvent . IsCancellationRequested )
2025-03-25 10:54:10 +01:00
{
2025-06-25 15:24:04 -03:00
Library . Logging . Log . WriteVerboseMessage ( LOGTAG , "TrayIconPollRequestError" , ex ,
"Failed to get response" );
2025-03-25 10:54:10 +01:00
return ;
2013-02-12 21:43:14 +00:00
}
catch ( Exception ex )
{
2024-06-20 14:22:49 +02:00
errorCount ++;
2025-02-08 17:31:49 +01:00
// More than 1 error, and we are disconnected
2025-03-25 10:54:10 +01:00
if ( errorCount >= 2 && OnStatusUpdated != null || ! hasConnected )
2025-06-25 15:24:04 -03:00
{
if ( OnStatusUpdated != null )
await OnStatusUpdated . Invoke ( new ServerStatusImpl (
LiveControlState . Paused ,
SuggestedStatusIcon . Disconnected ,
m_lastEventId ,
m_lastDataUpdateId ,
m_lastNotificationId )). ConfigureAwait ( false );
}
2013-02-12 21:43:14 +00:00
System . Diagnostics . Trace . WriteLine ( "Request error: " + ex . Message );
2025-06-25 15:24:04 -03:00
Library . Logging . Log . WriteWarningMessage ( LOGTAG , "TrayIconPollRequestError" , ex ,
"Failed to get response" );
2013-02-12 21:43:14 +00:00
}
}
}
2025-03-25 10:54:10 +01:00
private async Task ThreadRunner ()
2013-02-12 21:43:14 +00:00
{
2025-03-25 10:54:10 +01:00
while ( true )
2013-02-12 21:43:14 +00:00
{
try
{
2025-03-25 10:54:10 +01:00
var req = await m_workQueue . ReadAsync (). ConfigureAwait ( false );
2025-06-25 15:24:04 -03:00
await PerformRequestAsync < string >( req . Method , req . Endpoint , req . Body , req . Timeout )
. ConfigureAwait ( false );
2025-03-25 10:54:10 +01:00
}
catch ( Exception ex ) when ( ex . IsRetiredException () || m_stopToken . IsCancellationRequested )
{
2025-06-25 15:24:04 -03:00
Library . Logging . Log . WriteVerboseMessage ( LOGTAG , "TrayIconRequestError" , ex ,
"Failed to get response" );
2025-03-25 10:54:10 +01:00
return ;
2013-02-12 21:43:14 +00:00
}
catch ( Exception ex )
{
System . Diagnostics . Trace . WriteLine ( "Request error: " + ex . Message );
2025-06-25 15:24:04 -03:00
Library . Logging . Log . WriteWarningMessage ( LOGTAG , "TrayIconRequestError" , ex ,
"Failed to get response" );
2013-02-12 21:43:14 +00:00
}
}
}
public void Close ()
{
2025-03-25 10:54:10 +01:00
m_stopToken . Cancel ();
m_workQueue . Retire ();
2025-06-25 15:24:04 -03:00
if ( Interlocked . CompareExchange ( ref _connectionClosedInvoked , 1 , 0 ) == 0 )
ConnectionClosed ?. Invoke ();
2013-02-12 21:43:14 +00:00
}
2024-06-19 13:54:10 +02:00
private sealed record SigninResponse ( string AccessToken );
2014-06-27 13:23:05 +02:00
2025-07-08 13:03:33 +02:00
private async Task < T > PerformRequestAsync < T >( string method , string urlfragment , string? body , TimeSpan ? timeout )
2015-02-26 23:21:22 +01:00
{
2024-08-20 12:15:18 +02:00
if ( string . IsNullOrWhiteSpace ( m_accesstoken ) && ! urlfragment . StartsWith ( "/auth/" ))
2025-02-08 17:31:49 +01:00
await ObtainAccessTokenAsync (). ConfigureAwait ( false );
2014-06-27 13:23:05 +02:00
2025-02-08 17:31:49 +01:00
var hasTriedPassword = m_isTryingWithPassword ;
2015-02-26 23:21:22 +01:00
while ( true )
2014-06-27 13:23:05 +02:00
{
2015-02-26 23:21:22 +01:00
try
2014-06-27 13:23:05 +02:00
{
2025-06-25 15:24:04 -03:00
return await PerformRequestInternalAsync < T >( method , urlfragment , body , timeout )
. ConfigureAwait ( false );
2014-06-27 13:23:05 +02:00
}
2024-12-28 20:02:21 +01:00
catch ( Exception ex )
2025-06-25 15:24:04 -03:00
when ( ex is HttpRequestException hex && ( hex . StatusCode == HttpStatusCode . Unauthorized ||
hex . StatusCode == HttpStatusCode . Forbidden )
|| ex is AggregateException aex && aex . InnerExceptions . Any ( x =>
x is HttpRequestException hex && ( hex . StatusCode == HttpStatusCode . Unauthorized ||
hex . StatusCode == HttpStatusCode . Forbidden ))
)
2015-02-26 23:21:22 +01:00
{
2024-12-28 20:02:21 +01:00
if ( hasTriedPassword )
2015-02-26 23:21:22 +01:00
throw ;
2025-02-08 17:31:49 +01:00
// TODO: This error handling is error prone and can end up in infinite recursion
// Should rewrite the entire class and use websockets instead
2024-06-20 06:57:46 +02:00
// Only try once, and clear the token for the next try
2024-06-19 13:54:10 +02:00
hasTriedPassword = true ;
2024-06-20 06:57:46 +02:00
m_accesstoken = null ;
2025-02-08 17:31:49 +01:00
try
{
m_isTryingWithPassword = true ;
await ObtainAccessTokenAsync (). ConfigureAwait ( false );
}
finally
{
m_isTryingWithPassword = false ;
}
2015-02-26 23:21:22 +01:00
}
2014-06-27 13:23:05 +02:00
}
}
2025-02-08 17:31:49 +01:00
private async Task ObtainAccessTokenAsync ()
2013-02-12 21:43:14 +00:00
{
2025-02-10 10:22:16 +01:00
// If we host the server, issue the access token from the service
2025-05-04 12:13:15 +02:00
var sp = Server . Program . DuplicatiWebserver ?. Provider ;
if ( sp != null && m_passwordSource == Program . PasswordSource . HostedServer )
2025-02-10 10:22:16 +01:00
{
2025-05-04 12:13:15 +02:00
var provider = sp . GetRequiredService < IJWTTokenProvider >();
2025-02-10 10:22:16 +01:00
m_accesstoken = provider . CreateAccessToken ( "trayicon" , provider . TemporaryFamilyId );
return ;
}
// If we know the password, issue a token from the API
2025-02-09 11:28:52 +01:00
if (! string . IsNullOrWhiteSpace ( m_password ))
{
2025-06-25 15:24:04 -03:00
m_accesstoken = ( await PerformRequestInternalAsync < SigninResponse >( "POST" , "/auth/login" ,
JsonSerializer . Serialize ( new { Password = m_password }), null ). ConfigureAwait ( false )). AccessToken ;
2025-02-09 11:28:52 +01:00
return ;
}
2025-02-10 10:22:16 +01:00
// Otherwise, try to get a signin token
2025-02-08 17:31:49 +01:00
var token = await ObtainSignInTokenAsync (). ConfigureAwait ( false );
2024-06-19 13:54:10 +02:00
if ( string . IsNullOrWhiteSpace ( token ))
return ;
2025-02-09 11:28:52 +01:00
2025-02-10 10:22:16 +01:00
// Use the signin token to get an access token
2025-06-25 15:24:04 -03:00
m_accesstoken =
( await PerformRequestInternalAsync < SigninResponse >( "POST" , "/auth/signin" ,
JsonSerializer . Serialize ( new { SigninToken = token }), null ). ConfigureAwait ( false )). AccessToken ;
2024-06-19 13:54:10 +02:00
}
2013-02-12 21:43:14 +00:00
2025-07-08 13:03:33 +02:00
private async Task < T > PerformRequestInternalAsync < T >( string method , string endpoint , string? body ,
2025-06-25 15:24:04 -03:00
TimeSpan ? timeout )
2024-06-19 13:54:10 +02:00
{
2025-06-25 15:24:04 -03:00
var request = new HttpRequestMessage ( new HttpMethod ( method ), new Uri ( m_apiUri + endpoint ));
2024-06-19 13:54:10 +02:00
if (! string . IsNullOrWhiteSpace ( m_accesstoken ))
request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , m_accesstoken );
2017-09-18 23:23:45 -06:00
2024-06-19 13:54:10 +02:00
if (! string . IsNullOrWhiteSpace ( body ))
request . Content = new StringContent ( body , Encoding . UTF8 , "application/json" );
// Set up response timeout, use 100s which is the .NET default
2025-08-07 11:50:01 +02:00
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( m_stopToken . Token , _applicationExitEvent );
2024-06-19 13:54:10 +02:00
cts . CancelAfter ( timeout . HasValue
2025-06-27 11:19:24 -03:00
? timeout . Value + TimeSpan . FromSeconds ( 5 )
2024-06-19 13:54:10 +02:00
: TimeSpan . FromSeconds ( 100 ));
2025-08-07 11:50:01 +02:00
var response = await HTTPCLIENT . SendAsync ( request , cts . Token ). ConfigureAwait ( false );
2024-06-19 13:54:10 +02:00
response . EnsureSuccessStatusCode ();
2024-07-18 21:29:36 +02:00
if ( typeof ( T ) == typeof ( string ))
2025-06-25 15:24:04 -03:00
return ( T )( object ) await response . Content . ReadAsStringAsync ( cts . Token ). ConfigureAwait ( false );
2024-07-18 21:29:36 +02:00
2025-06-25 15:24:04 -03:00
await using var stream = await response . Content . ReadAsStreamAsync ( cts . Token ). ConfigureAwait ( false );
2025-07-08 13:03:33 +02:00
return await JsonSerializer . DeserializeAsync < T >( stream , serializerOptions , cts . Token ). ConfigureAwait ( false )
?? throw new JsonException ( "Failed to deserialize response." );
2013-02-12 21:43:14 +00:00
}
2025-07-08 13:03:33 +02:00
private void ExecuteAndNotify ( string method , string urifragment , string? body )
2013-02-12 21:43:14 +00:00
{
2025-03-25 10:54:10 +01:00
m_workQueue . WriteNoWait ( new BackgroundRequest ( method , urifragment , body , null ));
2013-02-12 21:43:14 +00:00
}
2025-07-08 13:03:33 +02:00
public void Pause ( string? duration = null )
2013-02-12 21:43:14 +00:00
{
2025-06-25 15:24:04 -03:00
ExecuteAndNotify ( "POST" ,
$"/serverstate/pause{(string.IsNullOrWhiteSpace(duration) ? "" : $" ? duration ={ duration } ")}" , null );
2013-02-12 21:43:14 +00:00
}
public void Resume ()
{
2024-06-19 13:54:10 +02:00
ExecuteAndNotify ( "POST" , "/serverstate/resume" , null );
2013-02-12 21:43:14 +00:00
}
2019-05-06 20:13:04 -07:00
2024-06-19 13:54:10 +02:00
public void Dispose ()
2019-05-06 20:13:03 -07:00
{
2024-06-19 13:54:10 +02:00
Close ();
2019-05-06 20:13:03 -07:00
}
2024-06-19 13:54:10 +02:00
private sealed record SigninTokenResponse ( string Token );
2025-06-25 15:24:04 -03:00
2025-02-08 17:31:49 +01:00
private async Task < string > IssueSigninTokenAsync ( string password )
2025-06-25 15:24:04 -03:00
=> ( await PerformRequestInternalAsync < SigninTokenResponse >( "POST" , "/auth/issuesignintoken" ,
JsonSerializer . Serialize ( new { Password = password }), null ). ConfigureAwait ( false )). Token ;
2019-05-06 20:13:01 -07:00
2025-07-08 13:03:33 +02:00
private async Task < string? > ObtainSignInTokenAsync ()
2013-02-12 21:43:14 +00:00
{
2025-07-08 13:03:33 +02:00
string? signinjwt = null ;
2024-06-19 13:54:10 +02:00
// If we host the server, issue the token from the service
2025-05-04 12:13:15 +02:00
var sp = Server . Program . DuplicatiWebserver ?. Provider ;
if ( sp != null && m_passwordSource == Program . PasswordSource . HostedServer )
2025-02-09 11:28:52 +01:00
{
2025-05-04 12:13:15 +02:00
if ( sp . GetRequiredService < Server . Database . Connection >(). ApplicationSettings . DisableSigninTokens )
2025-02-09 11:28:52 +01:00
return null ;
2025-05-04 12:13:15 +02:00
signinjwt = sp . GetRequiredService < IJWTTokenProvider >(). CreateSigninToken ( "trayicon" );
2025-02-09 11:28:52 +01:00
}
2024-06-19 13:54:10 +02:00
// If we have database access, grab the issuer key from the db and issue a token
if ( string . IsNullOrWhiteSpace ( signinjwt ) && m_passwordSource == Program . PasswordSource . Database )
{
Program . databaseConnection . ApplicationSettings . ReloadSettings ();
2025-02-09 11:28:52 +01:00
if ( Program . databaseConnection . ApplicationSettings . DisableSigninTokens )
return null ;
2024-06-19 13:54:10 +02:00
var cfg = Program . databaseConnection . ApplicationSettings . JWTConfig ;
if (! string . IsNullOrWhiteSpace ( cfg ))
2025-06-25 15:24:04 -03:00
signinjwt =
2025-07-08 13:03:33 +02:00
new JWTTokenProvider ( JsonSerializer . Deserialize < JWTConfig >( cfg ) ?? throw new JsonException ( "Failed to deserialize JWT config" )). CreateSigninToken ( "trayicon" );
2024-06-19 13:54:10 +02:00
}
// If we know the password, issue a token from the API
if ( string . IsNullOrWhiteSpace ( signinjwt ) && ! string . IsNullOrWhiteSpace ( m_password ))
2025-02-08 17:31:49 +01:00
signinjwt = await IssueSigninTokenAsync ( m_password );
2024-06-19 13:54:10 +02:00
return signinjwt ;
2013-02-12 21:43:14 +00:00
}
2024-06-19 13:54:10 +02:00
2025-02-08 17:31:49 +01:00
public async Task < string > GetStatusWindowURLAsync ()
2013-02-12 21:43:14 +00:00
{
2025-07-08 13:03:33 +02:00
string? signinjwt = null ;
2025-02-09 11:28:52 +01:00
if (! m_disableTrayIconLogin )
{
try
{
signinjwt = await ObtainSignInTokenAsync (). ConfigureAwait ( false );
}
catch
{
}
}
2025-02-08 17:31:49 +01:00
return string . IsNullOrWhiteSpace ( signinjwt )
? m_baseUri + STATUS_WINDOW
: m_baseUri + SIGNIN_WINDOW + $"?token={signinjwt}" ;
2013-02-12 21:43:14 +00:00
}
}
2025-06-27 11:19:24 -03:00
}