diff --git a/Duplicati/Library/RestAPI/Strings.cs b/Duplicati/Library/RestAPI/Strings.cs
index b773359f1..115f56e7b 100644
--- a/Duplicati/Library/RestAPI/Strings.cs
+++ b/Duplicati/Library/RestAPI/Strings.cs
@@ -73,6 +73,7 @@ Error message: {0}", error); }
public static string WebserverApiOnlyDescription { get { return LC.L(@"Disable the web interface and only allow API access"); } }
public static string WebserverDisableSigninTokensDescription { get { return LC.L(@"Disable the use of signin tokens"); } }
public static string WebserverSpaPathsDescription { get { return LC.L(@"The relative paths that should be served as single page applications, separated with semicolons."); } }
+ public static string WebserverCorsOriginsDescription { get { return LC.L(@"A list of CORS origins to allow, separated with semicolons. Each origin must be a valid URL."); } }
public static string WebserverTimezoneDescription { get { return LC.L(@"The timezone to use for the webserver. The timezone must be a valid timezone identifier, such as ""America/New_York"" or ""UTC"". Common three-letter abbreviations like ""CET"" are supported, but ambiguous in some cases."); } }
public static string DisabledbencryptionLong { get { return LC.L(@"Use this option to disable database encryption of sensitive fields"); } }
public static string DisabledbencryptionShort { get { return LC.L(@"Disable database encryption"); } }
diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs
index 7b2fb9c0a..94215b83c 100644
--- a/Duplicati/Server/Program.cs
+++ b/Duplicati/Server/Program.cs
@@ -403,7 +403,8 @@ namespace Duplicati.Server
parsedOptions.Servername,
parsedOptions.AllowedHostnames,
parsedOptions.DisableStaticFiles,
- parsedOptions.SPAPaths
+ parsedOptions.SPAPaths,
+ parsedOptions.CorsOrigins
);
var server = DuplicatiWebserver.CreateWebServer(mappedSettings, connection);
@@ -980,6 +981,7 @@ namespace Duplicati.Server
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverDisableSigninTokensDescription, Strings.Program.WebserverDisableSigninTokensDescription),
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_SPAPATHS, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverSpaPathsDescription, Strings.Program.WebserverSpaPathsDescription, WebServerLoader.DEFAULT_OPTION_SPAPATHS),
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_TIMEZONE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverTimezoneDescription, Strings.Program.WebserverTimezoneDescription, TimeZoneHelper.GetLocalTimeZone(), null, TimeZoneHelper.GetTimeZones().Select(x => x.Id).ToArray()),
+ new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_CORS_ORIGINS, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverCorsOriginsDescription, Strings.Program.WebserverCorsOriginsDescription, WebServerLoader.DEFAULT_OPTION_SPAPATHS),
new Duplicati.Library.Interface.CommandLineArgument(PING_PONG_KEEPALIVE_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.PingpongkeepaliveShort, Strings.Program.PingpongkeepaliveLong),
new Duplicati.Library.Interface.CommandLineArgument("log-retention", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan, Strings.Program.LogretentionShort, Strings.Program.LogretentionLong, DEFAULT_LOG_RETENTION),
new Duplicati.Library.Interface.CommandLineArgument("server-datafolder", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.ServerdatafolderShort, Strings.Program.ServerdatafolderLong(DATAFOLDER_ENV_NAME), System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Library.AutoUpdater.AutoUpdateSettings.AppName)),
diff --git a/Duplicati/Server/WebServerLoader.cs b/Duplicati/Server/WebServerLoader.cs
index 7f14dd5d0..3b82cf767 100644
--- a/Duplicati/Server/WebServerLoader.cs
+++ b/Duplicati/Server/WebServerLoader.cs
@@ -99,6 +99,10 @@ public static class WebServerLoader
///
public const string OPTION_WEBSERVICE_SPAPATHS = "webservice-spa-paths";
///
+ /// The CORS origins to allow
+ ///
+ public const string OPTION_WEBSERVICE_CORS_ORIGINS = "webservice-cors-origins";
+ ///
/// Option for setting the webservice timezone
///
public const string OPTION_WEBSERVICE_TIMEZONE = "webservice-timezone";
@@ -154,6 +158,7 @@ public static class WebServerLoader
/// The allowed hostnames
/// If static files should be disabled
/// The paths to serve as SPAs
+ /// The origins to allow for CORS
public record ParsedWebserverSettings(
string WebRoot,
int Port,
@@ -162,7 +167,8 @@ public static class WebServerLoader
string Servername,
IEnumerable AllowedHostnames,
bool DisableStaticFiles,
- IEnumerable SPAPaths
+ IEnumerable SPAPaths,
+ IEnumerable CorsOrigins
);
@@ -251,7 +257,8 @@ public static class WebServerLoader
string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, Library.AutoUpdater.UpdaterManager.SelfVersion.Version),
(connection.ApplicationSettings.AllowedHostnames ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries),
Duplicati.Library.Utility.Utility.ParseBoolOption(options, OPTION_WEBSERVICE_API_ONLY),
- spaPathsString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
+ spaPathsString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries),
+ options.GetValueOrDefault(OPTION_WEBSERVICE_CORS_ORIGINS)?.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? Enumerable.Empty()
);
// Materialize the list of ports, and move the last-used port to the front, so we try the last-known port first
diff --git a/Duplicati/WebserverCore/DuplicatiWebserver.cs b/Duplicati/WebserverCore/DuplicatiWebserver.cs
index 2010d70b4..9f5f0b4fc 100644
--- a/Duplicati/WebserverCore/DuplicatiWebserver.cs
+++ b/Duplicati/WebserverCore/DuplicatiWebserver.cs
@@ -48,6 +48,11 @@ public class DuplicatiWebserver
/// The log tag for this class
///
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType();
+ ///
+ /// The name of the CORS policy
+ ///
+ public const string CorsPolicyName = "CustomCorsPolicy";
+
///
/// The configuration for the server
///
@@ -68,6 +73,11 @@ public class DuplicatiWebserver
///
public required int Port { get; init; }
+ ///
+ /// The port the server is listening on
+ ///
+ public required bool CorsEnabled { get; init; }
+
///
/// The task that will be set when the server is terminated
///
@@ -93,6 +103,7 @@ public class DuplicatiWebserver
/// The allowed hostnames
/// If static files should be disabled
/// The paths to serve as SPAs
+ /// The origins to allow for CORS
public record InitSettings(
string WebRoot,
int Port,
@@ -101,7 +112,8 @@ public class DuplicatiWebserver
string Servername,
IEnumerable AllowedHostnames,
bool DisableStaticFiles,
- IEnumerable SPAPaths
+ IEnumerable SPAPaths,
+ IEnumerable CorsOrigins
);
///
@@ -253,6 +265,22 @@ public class DuplicatiWebserver
builder.Services.AddHttpClient();
+ var useCors = settings.CorsOrigins != null && settings.CorsOrigins.Any();
+ if (useCors)
+ {
+ builder.Services.AddCors(options =>
+ {
+ options.AddPolicy(
+ name: CorsPolicyName,
+ policy =>
+ {
+ policy.WithOrigins(settings.CorsOrigins!.ToArray())
+ .AllowAnyHeader()
+ .AllowAnyMethod();
+ });
+ });
+ }
+
var app = builder.Build();
HttpClientHelper.Configure(app.Services.GetRequiredService());
@@ -271,6 +299,9 @@ public class DuplicatiWebserver
if (!settings.DisableStaticFiles)
app.UseDefaultStaticFiles(settings.WebRoot, settings.SPAPaths);
+ if (useCors)
+ app.UseCors(CorsPolicyName);
+
app.UseExceptionHandler(app =>
{
app.Run(async context =>
@@ -308,7 +339,8 @@ public class DuplicatiWebserver
{
Configuration = builder.Configuration,
App = app,
- Port = settings.Port
+ Port = settings.Port,
+ CorsEnabled = useCors
};
}
@@ -345,7 +377,7 @@ public class DuplicatiWebserver
public Task Start()
{
App.MapHealthChecks("/health");
- App.AddEndpoints()
+ App.AddEndpoints(CorsEnabled)
.UseNotifications("/notifications");
return TerminationTask = App.RunAsync();
diff --git a/Duplicati/WebserverCore/Extensions/WebApplicationExtensions.cs b/Duplicati/WebserverCore/Extensions/WebApplicationExtensions.cs
index 00485cef9..254551ef9 100644
--- a/Duplicati/WebserverCore/Extensions/WebApplicationExtensions.cs
+++ b/Duplicati/WebserverCore/Extensions/WebApplicationExtensions.cs
@@ -26,12 +26,12 @@ namespace Duplicati.WebserverCore.Extensions;
public static class WebApplicationExtensions
{
- public static WebApplication AddEndpoints(this WebApplication application)
+ public static WebApplication AddEndpoints(this WebApplication application, bool useCors)
{
- return AddV1(application);
+ return AddV1(application, useCors);
}
- private static WebApplication AddV1(WebApplication application)
+ private static WebApplication AddV1(WebApplication application, bool useCors)
{
var mapperInterfaceType = typeof(IEndpointV1);
var endpoints =
@@ -46,6 +46,9 @@ public static class WebApplicationExtensions
if (!string.IsNullOrWhiteSpace(PreSharedKeyFilter.PreSharedKey))
group = group.AddEndpointFilter();
+ if (useCors)
+ group.RequireCors(DuplicatiWebserver.CorsPolicyName);
+
foreach (var endpoint in endpoints)
{
var methodMap = endpoint.GetMethod(nameof(IEndpointV1.Map), BindingFlags.Static | BindingFlags.Public);