From 9f7902574473639689fa7cea899b70f97b6b613e Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Fri, 7 Jun 2024 15:56:43 +0200 Subject: [PATCH] Removed `HttpServer`. Re-implemented everything using ASP.NET. Changed some requests to use JSON instead of FORM data. Some work towards deleting the FIXMEGlobal instance. Auth is missing, XSRF does not work correctly. --- .../Abstractions/IModernHttpRequestAccess.cs | 6 - .../Abstractions/IScheduler.cs | 4 +- .../Abstractions/IWorkerThreadsManager.cs | 2 + .../BackupImportExportHandler.cs | 133 ++++ Duplicati.Library.RestAPI/Database/Backup.cs | 18 +- .../Database/Connection.cs | 421 +++++----- .../Database/ServerSettings.cs | 112 +-- .../Duplicati.Library.RestAPI.csproj | 10 - Duplicati.Library.RestAPI/FIXMEGlobal.cs | 15 +- .../RESTMethods/Acknowledgements.cs | 57 -- .../RESTMethods/Backup.cs | 743 ------------------ .../RESTMethods/BackupDefaults.cs | 141 ---- .../RESTMethods/Backups.cs | 310 -------- .../RESTMethods/BugReport.cs | 62 -- .../RESTMethods/Captcha.cs | 172 ---- .../RESTMethods/Changelog.cs | 79 -- .../RESTMethods/CommandLine.cs | 293 ------- .../RESTMethods/Filesystem.cs | 296 ------- Duplicati.Library.RestAPI/RESTMethods/Help.cs | 104 --- .../RESTMethods/HyperV.cs | 93 --- .../RESTMethods/IRESTMethod.cs | 60 -- .../RESTMethods/Licenses.cs | 44 -- .../RESTMethods/LogData.cs | 122 --- .../RESTMethods/MSSQL.cs | 91 --- .../RESTMethods/Notification.cs | 65 -- .../RESTMethods/Notifications.cs | 34 - .../RESTMethods/ProgressState.cs | 50 -- .../RESTMethods/RemoteOperation.cs | 265 ------- .../RESTMethods/RequestInfo.cs | 117 --- .../RESTMethods/ServerSetting.cs | 116 --- .../RESTMethods/ServerSettings.cs | 136 ---- .../RESTMethods/ServerState.cs | 105 --- .../RESTMethods/SystemInfo.cs | 126 --- .../RESTMethods/SystemWideSettings.cs | 48 -- Duplicati.Library.RestAPI/RESTMethods/Tags.cs | 59 -- Duplicati.Library.RestAPI/RESTMethods/Task.cs | 113 --- .../RESTMethods/Tasks.cs | 41 - .../RESTMethods/UISettings.cs | 82 -- .../RESTMethods/Updates.cs | 44 -- .../RESTMethods/WebModule.cs | 45 -- .../RESTMethods/WebModules.cs | 34 - Duplicati.Library.RestAPI/Runner.cs | 2 +- Duplicati.Library.RestAPI/Scheduler.cs | 26 +- Duplicati.Library.RestAPI/SpecialFolders.cs | 18 +- Duplicati.Library.RestAPI/Strings.cs | 13 +- .../WebServer/AuthenticationHandler.cs | 342 -------- .../WebServer/BodyWriter.cs | 124 --- .../WebServer/CaptchaUtil.cs | 133 ---- .../WebServer/IndexHtmlHandler.cs | 82 -- .../WebServer/RESTHandler.cs | 263 ------- Duplicati.Library.RestAPI/WebServer/Server.cs | 441 ----------- .../SynologyAuthenticationHandler.cs | 293 ------- Duplicati.Library.RestAPI/newbackup.json | 28 +- .../ConfigurationImporter/Program.cs | 4 +- .../HostedInstanceKeeper.cs | 21 +- .../GUI/Duplicati.GUI.TrayIcon/Program.cs | 8 +- .../Utility/Abstractions/IBoolParser.cs | 6 - Duplicati/Library/Utility/BoolParser.cs | 11 - Duplicati/Library/Utility/WorkerThread.cs | 15 +- Duplicati/License/Duplicati.License.csproj | 14 +- Duplicati/License/LicenseReader.cs | 52 +- Duplicati/License/duplicati-url.txt | 1 + .../License/licenses/MailKit/Homepage.txt | 1 - .../License/licenses/MailKit/license.txt | 21 - .../License/licenses/MailKit/licensedata.json | 7 - Duplicati/License/licenses/duplicati-url.txt | 1 - .../Duplicati.Server.Serialization/Enums.cs | 68 +- .../Interface/IBackup.cs | 11 +- .../Interface/IDynamicModule.cs | 3 - .../Interface/IFilter.cs | 6 +- .../Interface/IProgressEventData.cs | 52 +- .../Interface/IQueuedTask.cs | 4 +- .../Interface/IServerSettings.cs | 4 +- .../Interface/ISetting.cs | 8 +- .../InterfaceResolver.cs | 53 +- .../Serializer.cs | 29 +- Duplicati/Server/Program.cs | 91 ++- Duplicati/Server/SingleInstance.cs | 52 +- Duplicati/Server/WebServerLoader.cs | 216 +++++ .../scripts/controllers/CaptchaController.js | 2 +- .../controllers/CommandlineController.js | 5 +- .../scripts/controllers/DeleteController.js | 2 +- .../controllers/EditBackupController.js | 10 +- .../scripts/controllers/ExportController.js | 2 +- .../scripts/controllers/ImportController.js | 1 + .../controllers/LocalDatabaseController.js | 10 +- .../scripts/controllers/RestoreController.js | 6 +- .../controllers/RestoreDirectController.js | 2 +- .../ngax/scripts/directives/backupEditUri.js | 4 +- .../scripts/directives/restoreFilePicker.js | 2 +- .../scripts/directives/sourceFolderPicker.js | 10 +- .../scripts/directives/targetFolderPicker.js | 5 +- .../ngax/scripts/services/AppService.js | 74 +- .../ngax/scripts/services/CaptchaService.js | 2 +- .../ngax/scripts/services/EditUriBuiltins.js | 28 +- .../Server/webroot/ngax/templates/import.html | 3 +- Duplicati/UnitTest/ImportExportTests.cs | 42 +- .../Abstractions/ICaptchaProvider.cs | 25 + .../Abstractions/ICommandlineRunService.cs | 24 + .../{IV1Endpoint.cs => IEndpointV1.cs} | 2 +- .../Abstractions/ILanguageService.cs | 6 + .../Abstractions/ISettingsService.cs | 1 + .../Abstractions/ServerSettings.cs | 149 +++- Duplicati/WebserverCore/AuthMiddleware.cs | 35 - .../Configuration/OptionConfiguration.cs | 14 - .../WebserverCore/Database/Entities/Option.cs | 9 - .../WebserverCore/Database/MainDbContext.cs | 14 - .../DayOfWeekStringEnumConverter.cs | 69 ++ .../WebserverCore/Dto/AcknowlegdementDto.cs | 6 + .../Dto/BackupAndScheduleInputDto.cs | 140 ++++ .../Dto/BackupAndScheduleOutputDto.cs | 17 + .../WebserverCore/Dto/BackupDefaultsDto.cs | 13 + Duplicati/WebserverCore/Dto/BackupDto.cs | 62 ++ Duplicati/WebserverCore/Dto/ChangelogDto.cs | 16 + .../Dto/CommandLineLogOutputDto.cs | 18 + .../Dto/CommandlineTaskStartedDto.cs | 8 + .../WebserverCore/Dto/CreateBackupDto.cs | 8 + .../Dto/DeleteBackupOutputDto.cs | 9 + .../WebserverCore/Dto/ExportArgsOnlyDto.cs | 9 + .../WebserverCore/Dto/ExportCommandlineDto.cs | 7 + Duplicati/WebserverCore/Dto/FilterDto.cs | 23 + Duplicati/WebserverCore/Dto/GetDbPathDto.cs | 8 + .../WebserverCore/Dto/GetTaskStateDto.cs | 10 + .../WebserverCore/Dto/IsBackupActiveDto.cs | 8 + .../WebserverCore/Dto/IsDbUsedElsewhereDto.cs | 7 + Duplicati/WebserverCore/Dto/LicenseDto.cs | 24 + .../WebserverCore/Dto/NotificationDto.cs | 64 ++ Duplicati/WebserverCore/Dto/RepairInputDto.cs | 10 + .../WebserverCore/Dto/RestoreInputDto.cs | 13 + Duplicati/WebserverCore/Dto/ScheduleDto.cs | 36 + Duplicati/WebserverCore/Dto/ServerStatus.cs | 24 - .../WebserverCore/Dto/ServerStatusDto.cs | 80 ++ Duplicati/WebserverCore/Dto/SettingDto.cs | 25 + .../WebserverCore/Dto/SolveCaptchaInputDto.cs | 8 + Duplicati/WebserverCore/Dto/SystemInfoDto.cs | 235 ++++++ Duplicati/WebserverCore/Dto/TaskStartedDto.cs | 8 + Duplicati/WebserverCore/Dto/TreeNodeDto.cs | 66 ++ .../WebserverCore/Dto/UpdateDbPathInputDto.cs | 7 + .../WebserverCore/Dto/WebModuleOutputDto.cs | 8 + .../Duplicati.WebserverCore.csproj | 14 +- Duplicati/WebserverCore/DuplicatiWebserver.cs | 132 +++- .../Endpoints/V1/Acknowledgements.cs | 21 + .../WebserverCore/Endpoints/V1/Antiforgery.cs | 17 + .../Endpoints/V1/Backup/BackupGet.cs | 264 +++++++ .../Endpoints/V1/Backup/BackupPost.cs | 174 ++++ .../Endpoints/V1/Backup/BackupPutDelete.cs | 200 +++++ .../Endpoints/V1/BackupDefaults.cs | 72 ++ .../WebserverCore/Endpoints/V1/Backups.cs | 238 ++++++ .../WebserverCore/Endpoints/V1/Bugreport.cs | 33 + .../WebserverCore/Endpoints/V1/Captcha.cs | 34 + .../WebserverCore/Endpoints/V1/Changelog.cs | 40 + .../WebserverCore/Endpoints/V1/Commandline.cs | 76 ++ .../WebserverCore/Endpoints/V1/Filesystem.cs | 281 +++++++ .../WebserverCore/Endpoints/V1/HyperV.cs | 87 ++ .../WebserverCore/Endpoints/V1/Licenses.cs | 34 + .../WebserverCore/Endpoints/V1/LogData.cs | 74 ++ Duplicati/WebserverCore/Endpoints/V1/MSSql.cs | 88 +++ .../Endpoints/V1/Notification.cs | 60 ++ .../Endpoints/V1/ProgressState.cs | 21 + .../Endpoints/V1/RemoteOperation.cs | 131 +++ .../Endpoints/V1/ServerSetting.cs | 125 +++ .../WebserverCore/Endpoints/V1/ServerState.cs | 62 ++ .../Endpoints/V1/ServerStateIv1Endpoint.cs | 11 - .../WebserverCore/Endpoints/V1/SystemInfo.cs | 79 ++ Duplicati/WebserverCore/Endpoints/V1/Tasks.cs | 94 +++ .../WebserverCore/Endpoints/V1/UISettings.cs | 24 + .../WebserverCore/Endpoints/V1/Updates.cs | 15 + .../WebserverCore/Endpoints/V1/WebModules.cs | 30 + .../Exceptions/BadRequestException.cs | 6 + .../Exceptions/ConflictException.cs | 7 + .../Exceptions/ForbiddenException.cs | 5 + .../Exceptions/NotFoundException.cs | 6 + .../Exceptions/ServerErrorException.cs | 6 + .../Exceptions/ServiceUnavailableException.cs | 7 + .../Exceptions/TextOutputErrorException.cs | 8 + .../Exceptions/UnauthorizedException.cs | 6 + .../Exceptions/UserReportedHttpException.cs | 9 + .../ServiceCollectionsExtensions.cs | 10 +- .../Extensions/WebApplicationExtensions.cs | 4 +- Duplicati/WebserverCore/LegacyHttpInput.cs | 61 -- .../WebserverCore/LegacyHttpRequestShim.cs | 197 ----- .../WebserverCore/LegacyHttpResponseShim.cs | 148 ---- .../WebserverCore/LegacyHttpSessionShim.cs | 33 - .../Middlewares/WebsocketExtensions.cs | 29 +- Duplicati/WebserverCore/RestHandlerCore.cs | 239 ------ .../WebserverCore/Services/CaptchaService.cs | 191 +++++ .../Services/CommandlineRunService.cs | 199 +++++ .../WebserverCore/Services/LanguageService.cs | 22 + .../Services/SchedulerService.cs | 35 + .../Services/Settings/SettingsService.cs | 105 +-- .../WebserverCore/Services/StatusService.cs | 8 +- .../WebserverCore/Services/UpdateService.cs | 2 +- .../Services}/WorkerThreadsManager.cs | 11 +- Duplicati/WebserverCore/appsettings.json | 25 +- thirdparty/HttpServer/Homepage.txt | 1 - thirdparty/HttpServer/HttpServer.dll | Bin 131584 -> 0 bytes thirdparty/HttpServer/license.txt | 69 -- thirdparty/HttpServer/licensedata.json | 7 - 198 files changed, 5281 insertions(+), 7633 deletions(-) delete mode 100644 Duplicati.Library.RestAPI/Abstractions/IModernHttpRequestAccess.cs create mode 100644 Duplicati.Library.RestAPI/BackupImportExportHandler.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Backup.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Backups.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/BugReport.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Captcha.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Changelog.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Help.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/HyperV.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Licenses.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/LogData.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Notification.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Notifications.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/ServerState.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Tags.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Task.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Tasks.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/UISettings.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/Updates.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/WebModule.cs delete mode 100644 Duplicati.Library.RestAPI/RESTMethods/WebModules.cs delete mode 100644 Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs delete mode 100644 Duplicati.Library.RestAPI/WebServer/BodyWriter.cs delete mode 100644 Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs delete mode 100644 Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs delete mode 100644 Duplicati.Library.RestAPI/WebServer/RESTHandler.cs delete mode 100644 Duplicati.Library.RestAPI/WebServer/Server.cs delete mode 100644 Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs delete mode 100644 Duplicati/Library/Utility/Abstractions/IBoolParser.cs delete mode 100644 Duplicati/Library/Utility/BoolParser.cs create mode 100644 Duplicati/License/duplicati-url.txt delete mode 100644 Duplicati/License/licenses/MailKit/Homepage.txt delete mode 100644 Duplicati/License/licenses/MailKit/license.txt delete mode 100644 Duplicati/License/licenses/MailKit/licensedata.json delete mode 100644 Duplicati/License/licenses/duplicati-url.txt create mode 100644 Duplicati/Server/WebServerLoader.cs create mode 100644 Duplicati/WebserverCore/Abstractions/ICaptchaProvider.cs create mode 100644 Duplicati/WebserverCore/Abstractions/ICommandlineRunService.cs rename Duplicati/WebserverCore/Abstractions/{IV1Endpoint.cs => IEndpointV1.cs} (79%) create mode 100644 Duplicati/WebserverCore/Abstractions/ILanguageService.cs delete mode 100644 Duplicati/WebserverCore/AuthMiddleware.cs delete mode 100644 Duplicati/WebserverCore/Database/Configuration/OptionConfiguration.cs delete mode 100644 Duplicati/WebserverCore/Database/Entities/Option.cs delete mode 100644 Duplicati/WebserverCore/Database/MainDbContext.cs create mode 100644 Duplicati/WebserverCore/DayOfWeekStringEnumConverter.cs create mode 100644 Duplicati/WebserverCore/Dto/AcknowlegdementDto.cs create mode 100644 Duplicati/WebserverCore/Dto/BackupAndScheduleInputDto.cs create mode 100644 Duplicati/WebserverCore/Dto/BackupAndScheduleOutputDto.cs create mode 100644 Duplicati/WebserverCore/Dto/BackupDefaultsDto.cs create mode 100644 Duplicati/WebserverCore/Dto/BackupDto.cs create mode 100644 Duplicati/WebserverCore/Dto/ChangelogDto.cs create mode 100644 Duplicati/WebserverCore/Dto/CommandLineLogOutputDto.cs create mode 100644 Duplicati/WebserverCore/Dto/CommandlineTaskStartedDto.cs create mode 100644 Duplicati/WebserverCore/Dto/CreateBackupDto.cs create mode 100644 Duplicati/WebserverCore/Dto/DeleteBackupOutputDto.cs create mode 100644 Duplicati/WebserverCore/Dto/ExportArgsOnlyDto.cs create mode 100644 Duplicati/WebserverCore/Dto/ExportCommandlineDto.cs create mode 100644 Duplicati/WebserverCore/Dto/FilterDto.cs create mode 100644 Duplicati/WebserverCore/Dto/GetDbPathDto.cs create mode 100644 Duplicati/WebserverCore/Dto/GetTaskStateDto.cs create mode 100644 Duplicati/WebserverCore/Dto/IsBackupActiveDto.cs create mode 100644 Duplicati/WebserverCore/Dto/IsDbUsedElsewhereDto.cs create mode 100644 Duplicati/WebserverCore/Dto/LicenseDto.cs create mode 100644 Duplicati/WebserverCore/Dto/NotificationDto.cs create mode 100644 Duplicati/WebserverCore/Dto/RepairInputDto.cs create mode 100644 Duplicati/WebserverCore/Dto/RestoreInputDto.cs create mode 100644 Duplicati/WebserverCore/Dto/ScheduleDto.cs delete mode 100644 Duplicati/WebserverCore/Dto/ServerStatus.cs create mode 100644 Duplicati/WebserverCore/Dto/ServerStatusDto.cs create mode 100644 Duplicati/WebserverCore/Dto/SettingDto.cs create mode 100644 Duplicati/WebserverCore/Dto/SolveCaptchaInputDto.cs create mode 100644 Duplicati/WebserverCore/Dto/SystemInfoDto.cs create mode 100644 Duplicati/WebserverCore/Dto/TaskStartedDto.cs create mode 100644 Duplicati/WebserverCore/Dto/TreeNodeDto.cs create mode 100644 Duplicati/WebserverCore/Dto/UpdateDbPathInputDto.cs create mode 100644 Duplicati/WebserverCore/Dto/WebModuleOutputDto.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Acknowledgements.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Antiforgery.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPost.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/BackupDefaults.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Backups.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Bugreport.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Captcha.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Changelog.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Commandline.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Filesystem.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/HyperV.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Licenses.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/LogData.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/MSSql.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Notification.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/ProgressState.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/RemoteOperation.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/ServerSetting.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/ServerState.cs delete mode 100644 Duplicati/WebserverCore/Endpoints/V1/ServerStateIv1Endpoint.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/SystemInfo.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Tasks.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/UISettings.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/Updates.cs create mode 100644 Duplicati/WebserverCore/Endpoints/V1/WebModules.cs create mode 100644 Duplicati/WebserverCore/Exceptions/BadRequestException.cs create mode 100644 Duplicati/WebserverCore/Exceptions/ConflictException.cs create mode 100644 Duplicati/WebserverCore/Exceptions/ForbiddenException.cs create mode 100644 Duplicati/WebserverCore/Exceptions/NotFoundException.cs create mode 100644 Duplicati/WebserverCore/Exceptions/ServerErrorException.cs create mode 100644 Duplicati/WebserverCore/Exceptions/ServiceUnavailableException.cs create mode 100644 Duplicati/WebserverCore/Exceptions/TextOutputErrorException.cs create mode 100644 Duplicati/WebserverCore/Exceptions/UnauthorizedException.cs create mode 100644 Duplicati/WebserverCore/Exceptions/UserReportedHttpException.cs delete mode 100644 Duplicati/WebserverCore/LegacyHttpInput.cs delete mode 100644 Duplicati/WebserverCore/LegacyHttpRequestShim.cs delete mode 100644 Duplicati/WebserverCore/LegacyHttpResponseShim.cs delete mode 100644 Duplicati/WebserverCore/LegacyHttpSessionShim.cs delete mode 100644 Duplicati/WebserverCore/RestHandlerCore.cs create mode 100644 Duplicati/WebserverCore/Services/CaptchaService.cs create mode 100644 Duplicati/WebserverCore/Services/CommandlineRunService.cs create mode 100644 Duplicati/WebserverCore/Services/LanguageService.cs create mode 100644 Duplicati/WebserverCore/Services/SchedulerService.cs rename {Duplicati.Library.RestAPI => Duplicati/WebserverCore/Services}/WorkerThreadsManager.cs (75%) delete mode 100644 thirdparty/HttpServer/Homepage.txt delete mode 100644 thirdparty/HttpServer/HttpServer.dll delete mode 100644 thirdparty/HttpServer/license.txt delete mode 100644 thirdparty/HttpServer/licensedata.json diff --git a/Duplicati.Library.RestAPI/Abstractions/IModernHttpRequestAccess.cs b/Duplicati.Library.RestAPI/Abstractions/IModernHttpRequestAccess.cs deleted file mode 100644 index 0249d0858..000000000 --- a/Duplicati.Library.RestAPI/Abstractions/IModernHttpRequestAccess.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Duplicati.Library.RestAPI.Abstractions; - -public interface IModernHttpRequestAccess -{ - public string GetQueryParam(string name); -} \ No newline at end of file diff --git a/Duplicati.Library.RestAPI/Abstractions/IScheduler.cs b/Duplicati.Library.RestAPI/Abstractions/IScheduler.cs index 0d7016e78..9ddadfe73 100644 --- a/Duplicati.Library.RestAPI/Abstractions/IScheduler.cs +++ b/Duplicati.Library.RestAPI/Abstractions/IScheduler.cs @@ -23,9 +23,9 @@ public interface IScheduler void Terminate(bool wait); /// - /// An event that is raised when the schedule changes + /// Subscribes to the event that is triggered when the schedule changes /// - event EventHandler NewSchedule; + void SubScribeToNewSchedule(Action handler); /// /// A snapshot copy of the current schedule list diff --git a/Duplicati.Library.RestAPI/Abstractions/IWorkerThreadsManager.cs b/Duplicati.Library.RestAPI/Abstractions/IWorkerThreadsManager.cs index 6e4aa3b15..fad7d506a 100644 --- a/Duplicati.Library.RestAPI/Abstractions/IWorkerThreadsManager.cs +++ b/Duplicati.Library.RestAPI/Abstractions/IWorkerThreadsManager.cs @@ -12,4 +12,6 @@ public interface IWorkerThreadsManager Tuple? CurrentTask { get; } WorkerThread? WorkerThread { get; } void UpdateThrottleSpeeds(); + + long AddTask(Runner.IRunnerData data, bool skipQueue = false); } \ No newline at end of file diff --git a/Duplicati.Library.RestAPI/BackupImportExportHandler.cs b/Duplicati.Library.RestAPI/BackupImportExportHandler.cs new file mode 100644 index 000000000..6c11b0b46 --- /dev/null +++ b/Duplicati.Library.RestAPI/BackupImportExportHandler.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Duplicati.Server.Serialization; +using Duplicati.Server.Serialization.Interface; + +namespace Duplicati.Library.RestAPI; + +public static class BackupImportExportHandler +{ + public static void RemovePasswords(IBackup backup) + { + backup.SanitizeSettings(); + backup.SanitizeTargetUrl(); + } + + public static byte[] ExportToJSON(IBackup backup, string passphrase) + { + Server.Serializable.ImportExportStructure ipx = FIXMEGlobal.DataConnection.PrepareBackupForExport(backup); + + byte[] data; + using (var ms = new System.IO.MemoryStream()) + { + using (var sw = new System.IO.StreamWriter(ms)) + { + Serializer.SerializeJson(sw, ipx, true); + + if (!string.IsNullOrWhiteSpace(passphrase)) + { + ms.Position = 0; + using (var ms2 = new System.IO.MemoryStream()) + { + using (var m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary())) + { + m.Encrypt(ms, ms2); + data = ms2.ToArray(); + } + } + } + else + { + data = ms.ToArray(); + } + } + } + + return data; + } + + public static Server.Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func getPassword, Dictionary advancedOptions) + { + // This removes the ID and DBPath from the backup configuration. + Server.Serializable.ImportExportStructure importedStructure = LoadConfiguration(configurationFile, importMetadata, getPassword); + + // This will create the Duplicati-server.sqlite database file if it doesn't exist. + using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions)) + { + if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists."); + } + + string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule); + if (!string.IsNullOrWhiteSpace(error)) + { + throw new InvalidOperationException(error); + } + + // This creates a new ID and DBPath. + connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule); + } + + return importedStructure; + } + + public static Server.Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func getPassword) + { + Server.Serializable.ImportExportStructure ipx; + + var buf = new byte[3]; + using (var fs = System.IO.File.OpenRead(filename)) + { + Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length); + + fs.Position = 0; + if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S') + { + using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary())) + { + using (var m2 = m.Decrypt(fs)) + { + using (var sr = new System.IO.StreamReader(m2)) + { + ipx = Serializer.Deserialize(sr); + } + } + } + } + else + { + using (var sr = new System.IO.StreamReader(fs)) + { + ipx = Serializer.Deserialize(sr); + } + } + } + + if (ipx.Backup == null) + { + throw new Exception("No backup found in document"); + } + + if (ipx.Backup.Metadata == null) + { + ipx.Backup.Metadata = new Dictionary(); + } + + if (!importMetadata) + { + ipx.Backup.Metadata.Clear(); + } + + ipx.Backup.ID = null; + ipx.Backup.DBPath = null; + + if (ipx.Schedule != null) + { + ipx.Schedule.ID = -1; + } + + return ipx; + } +} diff --git a/Duplicati.Library.RestAPI/Database/Backup.cs b/Duplicati.Library.RestAPI/Database/Backup.cs index 117253942..ea24b049f 100644 --- a/Duplicati.Library.RestAPI/Database/Backup.cs +++ b/Duplicati.Library.RestAPI/Database/Backup.cs @@ -46,7 +46,7 @@ namespace Duplicati.Server.Database { this.ID = null; } - + internal void LoadChildren(Connection con) { if (this.IsTemporary) @@ -65,7 +65,9 @@ namespace Duplicati.Server.Database this.Metadata = con.GetMetadata(id); } } - + + protected void SetDBPath(string path) => this.DBPath = path; + /// /// The backup ID /// @@ -90,27 +92,27 @@ namespace Duplicati.Server.Database /// The path to the local database /// public string DBPath { get; internal set; } - + /// /// The backup source folders and files /// public string[] Sources { get; set; } - + /// /// The backup settings /// public ISetting[] Settings { get; set; } - + /// /// The filters applied to the source files /// public IFilter[] Filters { get; set; } - + /// /// The backup metadata /// - public IDictionary Metadata { get; set; } - + public IDictionary Metadata { get; set; } + /// /// Gets a value indicating if this instance is not persisted to the database /// diff --git a/Duplicati.Library.RestAPI/Database/Connection.cs b/Duplicati.Library.RestAPI/Database/Connection.cs index bd1d6f35a..f68edc767 100644 --- a/Duplicati.Library.RestAPI/Database/Connection.cs +++ b/Duplicati.Library.RestAPI/Database/Connection.cs @@ -38,15 +38,15 @@ namespace Duplicati.Server.Database m_connection = connection; m_errorcmd = m_connection.CreateCommand(); m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)"; - for(var i = 0; i < 4; i++) + for (var i = 0; i < 4; i++) m_errorcmd.Parameters.Add(m_errorcmd.CreateParameter()); - + this.ApplicationSettings = new ServerSettings(this); } - + public void LogError(string backupid, string message, Exception ex) { - lock(m_lock) + lock (m_lock) { if (!long.TryParse(backupid, out long id)) id = -1; @@ -57,49 +57,50 @@ namespace Duplicati.Server.Database m_errorcmd.ExecuteNonQuery(); } } - - internal void ExecuteWithCommand(Action f) + + public void ExecuteWithCommand(Action f) { - lock(m_lock) - using(var cmd = m_connection.CreateCommand()) + lock (m_lock) + using (var cmd = m_connection.CreateCommand()) f(cmd); } - internal Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup) + public Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup) { var scheduleId = GetScheduleIDsFromTags(new string[] { "ID=" + backup.ID }); - return new Serializable.ImportExportStructure() { - CreatedByVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), - Backup = (Database.Backup)backup, - Schedule = (Database.Schedule)(scheduleId.Any() ? GetSchedule(scheduleId.First()) : null), - DisplayNames = SpecialFolders.GetSourceNames(backup) - }; + return new Serializable.ImportExportStructure() + { + CreatedByVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), + Backup = (Database.Backup)backup, + Schedule = (Database.Schedule)(scheduleId.Any() ? GetSchedule(scheduleId.First()) : null), + DisplayNames = SpecialFolders.GetSourceNames(backup) + }; } - + public string RegisterTemporaryBackup(IBackup backup) { - lock(m_lock) + lock (m_lock) { if (backup == null) throw new ArgumentNullException(nameof(backup)); if (backup.ID != null) throw new ArgumentException("Backup is already active, cannot make temporary"); - + backup.ID = Guid.NewGuid().ToString("D"); m_temporaryBackups.Add(backup.ID, (Backup)backup); return backup.ID; } } - + public void UnregisterTemporaryBackup(IBackup backup) { - lock(m_lock) + lock (m_lock) m_temporaryBackups.Remove(backup.ID); } public void UpdateTemporaryBackup(IBackup backup) { - lock(m_lock) + lock (m_lock) if (m_temporaryBackups.Remove(backup.ID)) m_temporaryBackups.Add(backup.ID, (Backup)backup); } @@ -108,8 +109,8 @@ namespace Duplicati.Server.Database { if (string.IsNullOrEmpty(id)) return null; - - lock(m_lock) + + lock (m_lock) { Backup b; m_temporaryBackups.TryGetValue(id, out b); @@ -118,10 +119,10 @@ namespace Duplicati.Server.Database } public ServerSettings ApplicationSettings { get; private set; } - + internal IDictionary GetMetadata(long id) { - lock(m_lock) + lock (m_lock) return ReadFromDb( (rd) => new KeyValuePair( ConvertToString(rd, 0), @@ -130,11 +131,11 @@ namespace Duplicati.Server.Database @"SELECT ""Name"", ""Value"" FROM ""Metadata"" WHERE ""BackupID"" = ? ", id) .ToDictionary((k) => k.Key, (k) => k.Value); } - + internal void SetMetadata(IDictionary values, long id, System.Data.IDbTransaction transaction) { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + lock (m_lock) + using (var tr = transaction == null ? m_connection.BeginTransaction() : null) { OverwriteAndUpdateDb( tr, @@ -143,29 +144,30 @@ namespace Duplicati.Server.Database @"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (?, ?, ?)", (f) => new object[] { id, f.Key, f.Value } ); - + if (tr != null) tr.Commit(); } } - + internal IFilter[] GetFilters(long id) { - lock(m_lock) + lock (m_lock) return ReadFromDb( - (rd) => (IFilter)new Filter() { - Order = ConvertToInt64(rd, 0), + (rd) => (IFilter)new Filter() + { + Order = ConvertToInt64(rd, 0), Include = ConvertToBoolean(rd, 1), Expression = ConvertToString(rd, 2) ?? "" }, @"SELECT ""Order"", ""Include"", ""Expression"" FROM ""Filter"" WHERE ""BackupID"" = ? ORDER BY ""Order"" ", id) .ToArray(); } - + internal void SetFilters(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + lock (m_lock) + using (var tr = transaction == null ? m_connection.BeginTransaction() : null) { OverwriteAndUpdateDb( tr, @@ -174,7 +176,7 @@ namespace Duplicati.Server.Database @"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (?, ?, ?, ?)", (f) => new object[] { id, f.Order, f.Include, f.Expression } ); - + if (tr != null) tr.Commit(); } @@ -182,9 +184,10 @@ namespace Duplicati.Server.Database public ISetting[] GetSettings(long id) { - lock(m_lock) + lock (m_lock) return ReadFromDb( - (rd) => (ISetting)new Setting() { + (rd) => (ISetting)new Setting() + { Filter = ConvertToString(rd, 0) ?? "", Name = ConvertToString(rd, 1) ?? "", Value = ConvertToString(rd, 2) ?? "" @@ -193,42 +196,43 @@ namespace Duplicati.Server.Database @"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = ?", id) .ToArray(); } - + internal void SetSettings(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + lock (m_lock) + using (var tr = transaction == null ? m_connection.BeginTransaction() : null) { OverwriteAndUpdateDb( tr, @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?", new object[] { id }, values, @"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (?, ?, ?, ?)", - (f) => { - if (Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER.Equals(f.Value)) + (f) => + { + if (FIXMEGlobal.PASSWORD_PLACEHOLDER.Equals(f.Value)) throw new Exception("Attempted to save a property with the placeholder password"); return new object[] { id, f.Filter ?? "", f.Name, f.Value ?? "" }; } - ); - + ); + if (tr != null) tr.Commit(); } } - + internal string[] GetSources(long id) { - lock(m_lock) + lock (m_lock) return ReadFromDb( (rd) => ConvertToString(rd, 0), @"SELECT ""Path"" FROM ""Source"" WHERE ""BackupID"" = ?", id) .ToArray(); } - + internal void SetSources(IEnumerable values, long id, System.Data.IDbTransaction transaction) { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + lock (m_lock) + using (var tr = transaction == null ? m_connection.BeginTransaction() : null) { OverwriteAndUpdateDb( tr, @@ -236,8 +240,8 @@ namespace Duplicati.Server.Database values, @"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (?, ?)", (f) => new object[] { id, f } - ); - + ); + if (tr != null) tr.Commit(); } @@ -247,16 +251,16 @@ namespace Duplicati.Server.Database { if (tags == null || tags.Length == 0) return new long[0]; - + if (tags.Length == 1 && tags[0].StartsWith("ID=", StringComparison.Ordinal)) return new long[] { long.Parse(tags[0].Substring("ID=".Length)) }; - - lock(m_lock) - using(var cmd = m_connection.CreateCommand()) + + lock (m_lock) + using (var cmd = m_connection.CreateCommand()) { var sb = new StringBuilder(); - - foreach(var t in tags) + + foreach (var t in tags) { if (sb.Length != 0) sb.Append(" OR "); @@ -268,12 +272,12 @@ namespace Duplicati.Server.Database } cmd.CommandText = @"SELECT ""ID"" FROM ""Backup"" WHERE " + sb; - + return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); } } - internal IBackup GetBackup(string id) + public IBackup GetBackup(string id) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); @@ -283,10 +287,11 @@ namespace Duplicati.Server.Database internal IBackup GetBackup(long id) { - lock(m_lock) + lock (m_lock) { var bk = ReadFromDb( - (rd) => new Backup { + (rd) => new Backup + { ID = ConvertToInt64(rd, 0).ToString(), Name = ConvertToString(rd, 1), Description = ConvertToString(rd, 2), @@ -296,20 +301,21 @@ namespace Duplicati.Server.Database }, @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = ?", id) .FirstOrDefault(); - + if (bk != null) bk.LoadChildren(this); - + return bk; } } - - internal ISchedule GetSchedule(long id) + + public ISchedule GetSchedule(long id) { - lock(m_lock) + lock (m_lock) { var bk = ReadFromDb( - (rd) => new Schedule { + (rd) => new Schedule + { ID = ConvertToInt64(rd, 0), Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), Time = ConvertToDateTime(rd, 2), @@ -324,7 +330,7 @@ namespace Duplicati.Server.Database } } - internal Boolean IsUnencryptedOrPassphraseStored(long id) + public bool IsUnencryptedOrPassphraseStored(long id) { lock (m_lock) { @@ -345,17 +351,17 @@ namespace Duplicati.Server.Database } } - internal long[] GetScheduleIDsFromTags(string[] tags) + public long[] GetScheduleIDsFromTags(string[] tags) { if (tags == null || tags.Length == 0) return new long[0]; - - lock(m_lock) - using(var cmd = m_connection.CreateCommand()) + + lock (m_lock) + using (var cmd = m_connection.CreateCommand()) { var sb = new StringBuilder(); - - foreach(var t in tags) + + foreach (var t in tags) { if (sb.Length != 0) sb.Append(" OR "); @@ -367,7 +373,7 @@ namespace Duplicati.Server.Database } cmd.CommandText = @"SELECT ""ID"" FROM ""Schedule"" WHERE " + sb; - + return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); } } @@ -387,7 +393,7 @@ namespace Duplicati.Server.Database if (item.Sources == null || item.Sources.Any(x => string.IsNullOrWhiteSpace(x)) || item.Sources.Length == 0) return "Invalid source list"; - + var disabled_encryption = false; var passphrase = string.Empty; var gpgAsymmetricEncryption = false; @@ -449,7 +455,8 @@ namespace Duplicati.Server.Database if (!string.IsNullOrWhiteSpace(s.Value) && s.Value.Contains("-")) return "The prefix cannot contain hyphens (-)"; } - else if (string.Equals(s.Name, "--gpg-encryption-command", StringComparison.OrdinalIgnoreCase)) { + else if (string.Equals(s.Name, "--gpg-encryption-command", StringComparison.OrdinalIgnoreCase)) + { gpgAsymmetricEncryption = string.Equals(s.Value, "--encrypt", StringComparison.OrdinalIgnoreCase); } } @@ -475,7 +482,7 @@ namespace Duplicati.Server.Database return null; } - internal void UpdateBackupDBPath(IBackup item, string path) + public void UpdateBackupDBPath(IBackup item, string path) { lock (m_lock) { @@ -485,9 +492,9 @@ namespace Duplicati.Server.Database { cmd.Transaction = tr; cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter) cmd.Parameters[0]).Value = path; + ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = path; cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter) cmd.Parameters[1]).Value = item.ID; + ((System.Data.IDbDataParameter)cmd.Parameters[1]).Value = item.ID; cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?"; cmd.ExecuteNonQuery(); @@ -502,7 +509,7 @@ namespace Duplicati.Server.Database private void AddOrUpdateBackup(IBackup item, bool updateSchedule, ISchedule schedule) { - lock(m_lock) + lock (m_lock) { bool update = item.ID != null; if (!update && item.DBPath == null) @@ -511,7 +518,7 @@ namespace Duplicati.Server.Database if (!System.IO.Directory.Exists(folder)) System.IO.Directory.CreateDirectory(folder); - for(var i = 0; i < 100; i++) + for (var i = 0; i < 100; i++) { var guess = System.IO.Path.Combine(folder, System.IO.Path.ChangeExtension(Duplicati.Library.Main.DatabaseLocator.GenerateRandomName(), ".sqlite")); if (!System.IO.File.Exists(guess)) @@ -520,12 +527,12 @@ namespace Duplicati.Server.Database break; } } - + if (item.DBPath == null) throw new Exception("Unable to generate a unique database file name"); } - using(var tr = m_connection.BeginTransaction()) + using (var tr = m_connection.BeginTransaction()) { OverwriteAndUpdateDb( tr, @@ -535,13 +542,14 @@ namespace Duplicati.Server.Database update ? @"UPDATE ""Backup"" SET ""Name""=?, ""Description""=?, ""Tags""=?, ""TargetURL""=? WHERE ""ID""=?" : @"INSERT INTO ""Backup"" (""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"") VALUES (?,?,?,?,?)", - (n) => { - - if (n.TargetURL.IndexOf(Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0) + (n) => + { + + if (n.TargetURL.IndexOf(FIXMEGlobal.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0) throw new Exception("Attempted to save a backup with the password placeholder"); if (update && long.Parse(n.ID) <= 0) throw new Exception("Invalid update, cannot update application settings through update method"); - + return new object[] { n.Name, n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome @@ -552,13 +560,13 @@ namespace Duplicati.Server.Database }); if (!update) - using(var cmd = m_connection.CreateCommand()) + using (var cmd = m_connection.CreateCommand()) { cmd.Transaction = tr; cmd.CommandText = @"SELECT last_insert_rowid();"; item.ID = ExecuteScalarInt64(cmd).ToString(); } - + var id = long.Parse(item.ID); if (long.Parse(item.ID) <= 0) @@ -568,10 +576,10 @@ namespace Duplicati.Server.Database SetSettings(item.Settings, id, tr); SetFilters(item.Filters, id, tr); SetMetadata(item.Metadata, id, tr); - + if (updateSchedule) { - var tags = new string[] { "ID=" + item.ID }; + var tags = new string[] { "ID=" + item.ID }; var existing = GetScheduleIDsFromTags(tags); if (schedule == null && existing.Any()) DeleteFromDb("Schedule", existing.First(), tr); @@ -584,30 +592,30 @@ namespace Duplicati.Server.Database cur.Repeat = schedule.Repeat; cur.Tags = schedule.Tags; cur.Time = schedule.Time; - + schedule = cur; } else { schedule.ID = -1; } - + schedule.Tags = tags; AddOrUpdateSchedule(schedule, tr); } } - + tr.Commit(); FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId(); FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); } } } - + internal void AddOrUpdateSchedule(ISchedule item) { - lock(m_lock) - using(var tr = m_connection.BeginTransaction()) + lock (m_lock) + using (var tr = m_connection.BeginTransaction()) { AddOrUpdateSchedule(item, tr); tr.Commit(); @@ -615,10 +623,10 @@ namespace Duplicati.Server.Database FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); } } - + private void AddOrUpdateSchedule(ISchedule item, System.Data.IDbTransaction tr) { - lock(m_lock) + lock (m_lock) { bool update = item.ID >= 0; OverwriteAndUpdateDb( @@ -637,9 +645,9 @@ namespace Duplicati.Server.Database n.Rule ?? "", update ? (object)item.ID : null }); - + if (!update) - using(var cmd = m_connection.CreateCommand()) + using (var cmd = m_connection.CreateCommand()) { cmd.Transaction = tr; cmd.CommandText = @"SELECT last_insert_rowid();"; @@ -652,10 +660,10 @@ namespace Duplicati.Server.Database { if (ID < 0) return; - - lock(m_lock) + + lock (m_lock) { - using(var tr = m_connection.BeginTransaction()) + using (var tr = m_connection.BeginTransaction()) { var existing = GetScheduleIDsFromTags(new string[] { "ID=" + ID.ToString() }); if (existing.Any()) @@ -669,7 +677,7 @@ namespace Duplicati.Server.Database DeleteFromDb("Source", ID, "BackupID", tr); DeleteFromDb("Backup", ID, tr); - + tr.Commit(); } } @@ -677,7 +685,7 @@ namespace Duplicati.Server.Database FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId(); FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); } - + public void DeleteBackup(IBackup backup) { if (backup.IsTemporary) @@ -685,32 +693,33 @@ namespace Duplicati.Server.Database else DeleteBackup(long.Parse(backup.ID)); } - + public void DeleteSchedule(long ID) { if (ID < 0) return; - - lock(m_lock) + + lock (m_lock) DeleteFromDb("Schedule", ID); - + FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId(); FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); } - + public void DeleteSchedule(ISchedule schedule) { DeleteSchedule(schedule.ID); } - + public IBackup[] Backups { get { - lock(m_lock) + lock (m_lock) { var lst = ReadFromDb( - (rd) => (IBackup)new Backup() { + (rd) => (IBackup)new Backup() + { ID = ConvertToInt64(rd, 0).ToString(), Name = ConvertToString(rd, 1), Description = ConvertToString(rd, 2), @@ -720,23 +729,24 @@ namespace Duplicati.Server.Database }, @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" ") .ToArray(); - - foreach(var n in lst) + + foreach (var n in lst) n.Metadata = GetMetadata(long.Parse(n.ID)); - - + + return lst; } } } - + public ISchedule[] Schedules { get { - lock(m_lock) + lock (m_lock) return ReadFromDb( - (rd) => (ISchedule)new Schedule() { + (rd) => (ISchedule)new Schedule() + { ID = ConvertToInt64(rd, 0), Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), Time = ConvertToDateTime(rd, 2), @@ -746,16 +756,16 @@ namespace Duplicati.Server.Database }, @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" ") .ToArray(); - } + } } - - + + public IFilter[] Filters { get { return GetFilters(ANY_BACKUP_ID); } set { SetFilters(value, ANY_BACKUP_ID); } } - + public ISetting[] Settings { get { return GetSettings(ANY_BACKUP_ID); } @@ -764,13 +774,13 @@ namespace Duplicati.Server.Database public INotification[] GetNotifications() { - lock(m_lock) + lock (m_lock) return ReadFromDb(null).Cast().ToArray(); } public bool DismissNotification(long id) { - lock(m_lock) + lock (m_lock) { var notifications = GetNotifications(); var cur = notifications.FirstOrDefault(x => x.ID == id); @@ -790,7 +800,7 @@ namespace Duplicati.Server.Database public void RegisterNotification(Serialization.NotificationType type, string title, string message, Exception ex, string backupid, string action, string logid, string messageid, string logtag, Func conflicthandler) { - lock(m_lock) + lock (m_lock) { var notification = new Notification() { @@ -810,7 +820,7 @@ namespace Duplicati.Server.Database var conflictResult = conflicthandler(notification, GetNotifications()); if (conflictResult == null) return; - + if (conflictResult != notification) DeleteFromDb(typeof(Notification).Name, conflictResult.ID); @@ -829,7 +839,7 @@ namespace Duplicati.Server.Database //Workaround to clean up the database after invalid settings update public void FixInvalidBackupId() { - using(var cmd = m_connection.CreateCommand()) + using (var cmd = m_connection.CreateCommand()) using (var tr = m_connection.BeginTransaction()) { cmd.Transaction = tr; @@ -858,7 +868,7 @@ namespace Duplicati.Server.Database public string[] GetUISettingsSchemes() { - lock(m_lock) + lock (m_lock) return ReadFromDb( (rd) => ConvertToString(rd, 0) ?? "", @"SELECT DISTINCT ""Scheme"" FROM ""UIStorage""") @@ -867,33 +877,34 @@ namespace Duplicati.Server.Database public IDictionary GetUISettings(string scheme) { - lock(m_lock) + lock (m_lock) return ReadFromDb( (rd) => new KeyValuePair( ConvertToString(rd, 0) ?? "", ConvertToString(rd, 1) ?? "" ), - @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = ?", + @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = ?", scheme) .GroupBy(x => x.Key) .ToDictionary(x => x.Key, x => x.Last().Value); } - + public void SetUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + lock (m_lock) + using (var tr = transaction == null ? m_connection.BeginTransaction() : null) { OverwriteAndUpdateDb( tr, @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ?", new object[] { scheme }, values, @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", - (f) => { + (f) => + { return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; } - ); - + ); + if (tr != null) tr.Commit(); } @@ -922,19 +933,20 @@ namespace Duplicati.Server.Database public TempFile[] GetTempFiles() { - lock(m_lock) + lock (m_lock) return ReadFromDb(null).ToArray(); } public void DeleteTempFile(long id) { - lock(m_lock) + lock (m_lock) DeleteFromDb(typeof(TempFile).Name, id); } public long RegisterTempFile(string origin, string path, DateTime expires) { - var tempfile = new TempFile() { + var tempfile = new TempFile() + { Timestamp = DateTime.Now, Origin = origin, Path = path, @@ -950,8 +962,8 @@ namespace Duplicati.Server.Database { var t = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(purgeDate); - using(var tr = m_connection.BeginTransaction()) - using(var cmd = m_connection.CreateCommand()) + using (var tr = m_connection.BeginTransaction()) + using (var cmd = m_connection.CreateCommand()) { cmd.Transaction = tr; cmd.CommandText = @"DELETE FROM ""ErrorLog"" WHERE ""Timestamp"" < ?"; @@ -962,7 +974,7 @@ namespace Duplicati.Server.Database tr.Commit(); } } - + private static DateTime ConvertToDateTime(System.Data.IDataReader rd, int index) { var unixTime = ConvertToInt64(rd, index); @@ -973,7 +985,7 @@ namespace Duplicati.Server.Database { return ConvertToInt64(rd, index) == 1; } - + private static string ConvertToString(System.Data.IDataReader rd, int index) { var r = rd.GetValue(index); @@ -996,13 +1008,13 @@ namespace Duplicati.Server.Database private static long ExecuteScalarInt64(System.Data.IDbCommand cmd, long defaultValue = -1) { - using(var rd = cmd.ExecuteReader()) + using (var rd = cmd.ExecuteReader()) return rd.Read() ? ConvertToInt64(rd, 0) : defaultValue; } private static string ExecuteScalarString(System.Data.IDbCommand cmd) { - using(var rd = cmd.ExecuteReader()) + using (var rd = cmd.ExecuteReader()) return rd.Read() ? ConvertToString(rd, 0) : null; } @@ -1029,9 +1041,9 @@ namespace Duplicati.Server.Database // New function that allows to delete rows from tables with arbitrary identifier values (e.g. ID or BackupID) private bool DeleteFromDb(string tablename, long id, string identifier, System.Data.IDbTransaction transaction = null) { - if (transaction == null) + if (transaction == null) { - using(var tr = m_connection.BeginTransaction()) + using (var tr = m_connection.BeginTransaction()) { var r = DeleteFromDb(tablename, id, tr); tr.Commit(); @@ -1040,14 +1052,14 @@ namespace Duplicati.Server.Database } else { - using(var cmd = m_connection.CreateCommand()) + using (var cmd = m_connection.CreateCommand()) { cmd.Transaction = transaction; cmd.CommandText = string.Format(@"DELETE FROM ""{0}"" WHERE ""{1}""=?", tablename, identifier); var p = cmd.CreateParameter(); p.Value = id; cmd.Parameters.Add(p); - + var r = cmd.ExecuteNonQuery(); // Roll back the transaction if more than 1 ID was deleted. Multiple "BackupID" rows being deleted isn't a problem. if (identifier == "ID" && r > 1) @@ -1056,24 +1068,24 @@ namespace Duplicati.Server.Database } } } - + private static IEnumerable Read(System.Data.IDbCommand cmd, Func f) { - using(var rd = cmd.ExecuteReader()) - while(rd.Read()) + using (var rd = cmd.ExecuteReader()) + while (rd.Read()) yield return f(rd); } - + private static IEnumerable Read(System.Data.IDataReader rd, Func f) { - while(rd.Read()) + while (rd.Read()) yield return f(); } private System.Reflection.PropertyInfo[] GetORMFields() { - var flags = - System.Reflection.BindingFlags.FlattenHierarchy | + var flags = + System.Reflection.BindingFlags.FlattenHierarchy | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public; @@ -1084,10 +1096,10 @@ namespace Duplicati.Server.Database typeof(DateTime) }; - return + return (from n in typeof(T).GetProperties(flags) - where supportedPropertyTypes.Contains(n.PropertyType) || n.PropertyType.IsEnum - select n).ToArray(); + where supportedPropertyTypes.Contains(n.PropertyType) || n.PropertyType.IsEnum + select n).ToArray(); } private IEnumerable ReadFromDb(string whereclause, params object[] args) @@ -1102,26 +1114,27 @@ namespace Duplicati.Server.Database whereclause ?? "" ); - return ReadFromDb((rd) => { - var item = Activator.CreateInstance(); - for(var i = 0; i < properties.Length; i++) - { - var prop = properties[i]; + return ReadFromDb((rd) => + { + var item = Activator.CreateInstance(); + for (var i = 0; i < properties.Length; i++) + { + var prop = properties[i]; - if (prop.PropertyType.IsEnum) - prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null); - else if (prop.PropertyType == typeof(string)) - prop.SetValue(item, ConvertToString(rd, i), null); - else if (prop.PropertyType == typeof(long)) - prop.SetValue(item, ConvertToInt64(rd, i), null); - else if (prop.PropertyType == typeof(bool)) - prop.SetValue(item, ConvertToBoolean(rd, i), null); - else if (prop.PropertyType == typeof(DateTime)) - prop.SetValue(item, ConvertToDateTime(rd, i), null); - } + if (prop.PropertyType.IsEnum) + prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null); + else if (prop.PropertyType == typeof(string)) + prop.SetValue(item, ConvertToString(rd, i), null); + else if (prop.PropertyType == typeof(long)) + prop.SetValue(item, ConvertToInt64(rd, i), null); + else if (prop.PropertyType == typeof(bool)) + prop.SetValue(item, ConvertToBoolean(rd, i), null); + else if (prop.PropertyType == typeof(DateTime)) + prop.SetValue(item, ConvertToDateTime(rd, i), null); + } - return item; - }, sql, args); + return item; + }, sql, args); } private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, bool updateExisting) @@ -1144,7 +1157,7 @@ namespace Duplicati.Server.Database } else { - + sql = string.Format( @"INSERT INTO ""{0}"" (""{1}"") VALUES ({2})", typeof(T).Name, @@ -1164,12 +1177,12 @@ namespace Duplicati.Server.Database else if (x.PropertyType == typeof(DateTime)) val = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds((DateTime)val); - return val; + return val; }).ToArray(); }); if (!updateExisting && values.Count() == 1 && idfield != null) - using(var cmd = m_connection.CreateCommand()) + using (var cmd = m_connection.CreateCommand()) { cmd.Transaction = transaction; cmd.CommandText = @"SELECT last_insert_rowid();"; @@ -1179,64 +1192,64 @@ namespace Duplicati.Server.Database idfield.SetValue(values.First(), ExecuteScalarInt64(cmd), null); } } - + private IEnumerable ReadFromDb(Func f, string sql, params object[] args) { - using(var cmd = m_connection.CreateCommand()) + using (var cmd = m_connection.CreateCommand()) { cmd.CommandText = sql; if (args != null) - foreach(var a in args) + foreach (var a in args) { var p = cmd.CreateParameter(); p.Value = a; cmd.Parameters.Add(p); } - + return Read(cmd, f).ToArray(); } } - + private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, string insertSql, Func f) { - using(var cmd = m_connection.CreateCommand()) + using (var cmd = m_connection.CreateCommand()) { cmd.Transaction = transaction; - + if (!string.IsNullOrEmpty(deleteSql)) { cmd.CommandText = deleteSql; if (deleteArgs != null) - foreach(var a in deleteArgs) + foreach (var a in deleteArgs) { var p = cmd.CreateParameter(); p.Value = a; cmd.Parameters.Add(p); } - + cmd.ExecuteNonQuery(); cmd.Parameters.Clear(); } - + cmd.CommandText = insertSql; - - foreach(var n in values) + + foreach (var n in values) { var r = f(n); if (r == null) continue; - + while (cmd.Parameters.Count < r.Length) cmd.Parameters.Add(cmd.CreateParameter()); - - for(var i = 0; i < r.Length; i++) + + for (var i = 0; i < r.Length; i++) ((System.Data.IDbDataParameter)cmd.Parameters[i]).Value = r[i]; - + cmd.ExecuteNonQuery(); } } } - + #region IDisposable implementation public void Dispose() { @@ -1257,6 +1270,6 @@ namespace Duplicati.Server.Database } #endregion } - + } diff --git a/Duplicati.Library.RestAPI/Database/ServerSettings.cs b/Duplicati.Library.RestAPI/Database/ServerSettings.cs index 7f6fe2194..20ad3b471 100644 --- a/Duplicati.Library.RestAPI/Database/ServerSettings.cs +++ b/Duplicati.Library.RestAPI/Database/ServerSettings.cs @@ -50,10 +50,10 @@ namespace Duplicati.Server.Database public const string HAS_FIXED_INVALID_BACKUPID = "has-fixed-invalid-backup-id"; public const string UPDATE_CHANNEL = "update-channel"; public const string USAGE_REPORTER_LEVEL = "usage-reporter-level"; - public const string HAS_ASKED_FOR_PASSWORD_PROTECTION = "has-asked-for-password-protection"; + public const string HAS_ASKED_FOR_PASSWORD_PROTECTION = "has-asked-for-password-protection"; public const string DISABLE_TRAY_ICON_LOGIN = "disable-tray-icon-login"; public const string SERVER_ALLOWED_HOSTNAMES = "allowed-hostnames"; - } + } private readonly Dictionary settings; private readonly Connection databaseConnection; @@ -68,12 +68,12 @@ namespace Duplicati.Server.Database public void ReloadSettings() { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) { settings.Clear(); - foreach(var n in typeof(CONST).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Static).Select(x => (string)x.GetValue(null))) + foreach (var n in typeof(CONST).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Static).Select(x => (string)x.GetValue(null))) settings[n] = null; - foreach(var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID)) + foreach (var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID)) settings[n.Name] = n.Value; } } @@ -83,13 +83,13 @@ namespace Duplicati.Server.Database if (newsettings == null) throw new ArgumentNullException(); - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) { m_latestUpdate = null; if (clearExisting) settings.Clear(); - foreach(var k in newsettings) + foreach (var k in newsettings) if (!clearExisting && newsettings[k.Key] == null && k.Key.StartsWith("--", StringComparison.Ordinal)) settings.Remove(k.Key); else @@ -98,44 +98,48 @@ namespace Duplicati.Server.Database } SaveSettings(); - + if (newsettings.Keys.Contains(CONST.SERVER_PASSPHRASE)) GenerateWebserverPasswordTrayIcon(); } - + private void SaveSettings() { databaseConnection.SetSettings( from n in settings - select (Duplicati.Server.Serialization.Interface.ISetting)new Setting() { + select (Duplicati.Server.Serialization.Interface.ISetting)new Setting() + { Filter = "", Name = n.Key, Value = n.Value - }, Database.Connection.SERVER_SETTINGS_ID); + }, Database.Connection.SERVER_SETTINGS_ID); - FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId(); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + if (FIXMEGlobal.IsServerStarted) + { + FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + // If throttle options were changed, update now + FIXMEGlobal.WorkerThreadsManager.UpdateThrottleSpeeds(); + } - // In case the usage reporter is enabled or disabled, refresh now - FIXMEGlobal.StartOrStopUsageReporter(); - // If throttle options were changed, update now - FIXMEGlobal.WorkerThreadsManager.UpdateThrottleSpeeds(); + // In case the usage reporter is enabled or disabled, refresh now + FIXMEGlobal.StartOrStopUsageReporter(); } - + public string StartupDelayDuration { - get + get { return settings[CONST.STARTUP_DELAY]; } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.STARTUP_DELAY] = value; SaveSettings(); } } - + public System.Threading.ThreadPriority? ThreadPriorityOverride { get @@ -143,43 +147,43 @@ namespace Duplicati.Server.Database var tp = settings[CONST.THREAD_PRIORITY]; if (string.IsNullOrEmpty(tp)) return null; - - System.Threading.ThreadPriority r; + + System.Threading.ThreadPriority r; if (Enum.TryParse(tp, true, out r)) return r; - + return null; } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.THREAD_PRIORITY] = value.HasValue ? Enum.GetName(typeof(System.Threading.ThreadPriority), value.Value) : null; } } - + public string DownloadSpeedLimit { - get + get { return settings[CONST.DOWNLOAD_SPEED_LIMIT]; } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.DOWNLOAD_SPEED_LIMIT] = value; SaveSettings(); } } - + public string UploadSpeedLimit { - get + get { return settings[CONST.UPLOAD_SPEED_LIMIT]; } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.UPLOAD_SPEED_LIMIT] = value; SaveSettings(); } @@ -193,7 +197,7 @@ namespace Duplicati.Server.Database } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.IS_FIRST_RUN] = value.ToString(); SaveSettings(); } @@ -221,7 +225,7 @@ namespace Duplicati.Server.Database } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.UNACKED_ERROR] = value.ToString(); SaveSettings(); } @@ -235,7 +239,7 @@ namespace Duplicati.Server.Database } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.UNACKED_WARNING] = value.ToString(); SaveSettings(); } @@ -249,7 +253,7 @@ namespace Duplicati.Server.Database } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.SERVER_PORT_CHANGED] = value.ToString(); SaveSettings(); } @@ -282,7 +286,7 @@ namespace Duplicati.Server.Database } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.LAST_WEBSERVER_PORT] = value.ToString(); SaveSettings(); } @@ -290,7 +294,7 @@ namespace Duplicati.Server.Database public string WebserverPassword { - get + get { return settings[CONST.SERVER_PASSPHRASE]; } @@ -298,7 +302,7 @@ namespace Duplicati.Server.Database public string WebserverPasswordSalt { - get + get { return settings[CONST.SERVER_PASSPHRASE_SALT]; } @@ -308,7 +312,7 @@ namespace Duplicati.Server.Database { if (string.IsNullOrWhiteSpace(password)) { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) { settings[CONST.SERVER_PASSPHRASE] = ""; settings[CONST.SERVER_PASSPHRASE_SALT] = ""; @@ -328,7 +332,7 @@ namespace Duplicati.Server.Database sha256.TransformFinalBlock(buf, 0, buf.Length); var pwd = Convert.ToBase64String(sha256.Hash); - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) { settings[CONST.SERVER_PASSPHRASE] = pwd; settings[CONST.SERVER_PASSPHRASE_SALT] = salt; @@ -369,7 +373,7 @@ namespace Duplicati.Server.Database sha256.TransformFinalBlock(buf, 0, buf.Length); pwd = Convert.ToBase64String(sha256.Hash); } - + lock (databaseConnection.m_lock) { settings[CONST.SERVER_PASSPHRASETRAYICON] = password; @@ -381,7 +385,7 @@ namespace Duplicati.Server.Database public DateTime LastUpdateCheck { - get + get { long t; if (long.TryParse(settings[CONST.UPDATE_CHECK_LAST], out t)) @@ -391,7 +395,7 @@ namespace Duplicati.Server.Database } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.UPDATE_CHECK_LAST] = value.ToUniversalTime().Ticks.ToString(); SaveSettings(); } @@ -409,7 +413,7 @@ namespace Duplicati.Server.Database } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.UPDATE_CHECK_INTERVAL] = value; SaveSettings(); FIXMEGlobal.UpdatePoller.Reschedule(); @@ -443,7 +447,7 @@ namespace Duplicati.Server.Database if (m_latestUpdate != null) return m_latestUpdate; - using(var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION])) + using (var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION])) return m_latestUpdate = Server.Serialization.Serializer.Deserialize(tr); } catch @@ -458,14 +462,14 @@ namespace Duplicati.Server.Database if (value != null) { var sb = new System.Text.StringBuilder(); - using(var tw = new System.IO.StringWriter(sb)) + using (var tw = new System.IO.StringWriter(sb)) Server.Serialization.Serializer.SerializeJson(tw, value); result = sb.ToString(); } m_latestUpdate = value; - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.UPDATE_CHECK_NEW_VERSION] = result; SaveSettings(); @@ -474,13 +478,13 @@ namespace Duplicati.Server.Database public string ServerListenInterface { - get + get { return settings[CONST.SERVER_LISTEN_INTERFACE]; } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.SERVER_LISTEN_INTERFACE] = value; SaveSettings(); } @@ -526,7 +530,7 @@ namespace Duplicati.Server.Database } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.HAS_FIXED_INVALID_BACKUPID] = value.ToString(); SaveSettings(); } @@ -534,13 +538,13 @@ namespace Duplicati.Server.Database public string UpdateChannel { - get + get { return settings[CONST.UPDATE_CHANNEL]; } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.UPDATE_CHANNEL] = value; SaveSettings(); } @@ -548,13 +552,13 @@ namespace Duplicati.Server.Database public string UsageReporterLevel { - get + get { return settings[CONST.USAGE_REPORTER_LEVEL]; } set { - lock(databaseConnection.m_lock) + lock (databaseConnection.m_lock) settings[CONST.USAGE_REPORTER_LEVEL] = value; SaveSettings(); } diff --git a/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj index 3e01d4bd5..c09222ddf 100644 --- a/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj +++ b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj @@ -13,18 +13,8 @@ - - - - ..\Executables\net8\Duplicati.Server\bin\Debug\net8.0\Duplicati.WebserverCore.dll - - - ..\thirdparty\HttpServer\HttpServer.dll - - - diff --git a/Duplicati.Library.RestAPI/FIXMEGlobal.cs b/Duplicati.Library.RestAPI/FIXMEGlobal.cs index 3cb688507..730b5c79e 100644 --- a/Duplicati.Library.RestAPI/FIXMEGlobal.cs +++ b/Duplicati.Library.RestAPI/FIXMEGlobal.cs @@ -2,7 +2,6 @@ using Duplicati.Server; using System; using System.Collections.Generic; -using Duplicati.Library.IO; using Duplicati.Library.RestAPI.Abstractions; using Duplicati.Library.Utility; using Duplicati.WebserverCore.Abstractions; @@ -16,8 +15,13 @@ namespace Duplicati.Library.RestAPI */ public static class FIXMEGlobal { + /// + /// The placeholder for passwords in the UI + /// + public const string PASSWORD_PLACEHOLDER = "**********"; + public static IServiceProvider Provider { get; set; } - + /// /// This is the only access to the database /// @@ -42,6 +46,10 @@ namespace Duplicati.Library.RestAPI /// For keeping and incrementing last last events Ids of db save and last notification /// public static INotificationUpdateService NotificationUpdateService => Provider.GetRequiredService(); + /// + /// Checks if the server has started and is listening for events + /// + public static bool IsServerStarted => Provider != null; /// /// This is the working thread @@ -69,7 +77,7 @@ namespace Duplicati.Library.RestAPI /// public static readonly LogWriteHandler LogHandler = new LogWriteHandler(); - public static Func, Server.Database.Connection> GetDatabaseConnection; + public static Func, Server.Database.Connection> GetDatabaseConnection; /// /// The update poll thread. @@ -100,6 +108,5 @@ namespace Duplicati.Library.RestAPI /// public static readonly object MainLock = new object(); - private static LiveControls _liveControl; } } diff --git a/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs b/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs deleted file mode 100644 index f9b558c5b..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs +++ /dev/null @@ -1,57 +0,0 @@ -// 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; -using System.Collections.Generic; -using Duplicati.Library.Common.IO; -using Duplicati.Library.Utility; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Acknowledgements : IRESTMethodGET, IRESTMethodDocumented - { - private class GetResponse - { - public string Status; - public string Acknowledgements; - } - - public void GET(string key, RequestInfo info) - { - var path = SystemIO.IO_OS.PathCombine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "acknowledgements.txt"); - info.OutputOK(new GetResponse() { - Status = "OK", - Acknowledgements = System.IO.File.ReadAllText(path) - }); - } - public string Description { get { return "Gets all acknowledgements"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Backup.cs b/Duplicati.Library.RestAPI/RESTMethods/Backup.cs deleted file mode 100644 index c69473c6c..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Backup.cs +++ /dev/null @@ -1,743 +0,0 @@ -// 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; -using System.Collections.Generic; -using Duplicati.Server.Serialization; -using System.IO; -using System.Linq; -using Duplicati.Server.Serialization.Interface; -using Duplicati.Library.RestAPI; -using Duplicati.Library.RestAPI.Abstractions; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Backup : IRESTMethodGET, IRESTMethodPUT, IRESTMethodPOST, IRESTMethodDELETE, IRESTMethodDocumented - { - public class GetResponse - { - public class GetResponseData - { - public Serialization.Interface.ISchedule Schedule; - public Serialization.Interface.IBackup Backup; - public Dictionary DisplayNames; - } - - public bool success; - - public GetResponseData data; - } - - private void SearchFiles(IBackup backup, string filterstring, RequestInfo info) - { - var filter = filterstring; - var timestring = info.Request.QueryString["time"].Value; - var allversion = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["all-versions"].Value, false); - - if (string.IsNullOrWhiteSpace(timestring) && !allversion) - { - info.ReportClientError("Invalid or missing time", System.Net.HttpStatusCode.BadRequest); - return; - } - - var prefixonly = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["prefix-only"].Value, false); - var foldercontents = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["folder-contents"].Value, false); - var time = new DateTime(); - if (!allversion) - time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now); - - var r = Runner.Run(Runner.CreateListTask(backup, new string[] { filter }, prefixonly, allversion, foldercontents, time), false) as Duplicati.Library.Interface.IListResults; - - var result = new Dictionary(); - - foreach(HttpServer.HttpInputItem n in info.Request.QueryString) - result[n.Name] = n.Value; - - result["Filesets"] = r.Filesets; - result["Files"] = r.Files - // Group directories first - support either directory separator here as we may be restoring data from an alternate platform - .OrderByDescending(f => (f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("/", StringComparison.Ordinal)) || (!f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("\\", StringComparison.Ordinal))) - // Sort both groups (directories and files) alphabetically - .ThenBy(f => f.Path); - - info.OutputOK(result); - - } - - private void ListFileSets(IBackup backup, RequestInfo info) - { - var input = info.Request.QueryString; - var extra = new Dictionary - { - ["list-sets-only"] = "true" - }; - if (input["include-metadata"].Value != null) - extra["list-sets-only"] = (!Library.Utility.Utility.ParseBool(input["include-metadata"].Value, false)).ToString(); - if (input["from-remote-only"].Value != null) - extra["no-local-db"] = Library.Utility.Utility.ParseBool(input["from-remote-only"].Value, false).ToString(); - - var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, backup, extra), false) as Duplicati.Library.Interface.IListResults; - - if (r.EncryptedFiles && backup.Settings.Any(x => string.Equals("--no-encryption", x.Name, StringComparison.OrdinalIgnoreCase))) - info.ReportServerError("encrypted-storage"); - else - info.OutputOK(r.Filesets); - } - - private void FetchLogData(IBackup backup, RequestInfo info) - { - using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath)) - using(var cmd = con.CreateCommand()) - info.OutputOK(LogData.DumpTable(cmd, "LogData", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value)); - } - - private void FetchRemoteLogData(IBackup backup, RequestInfo info) - { - using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath)) - using(var cmd = con.CreateCommand()) - { - var dt = LogData.DumpTable(cmd, "RemoteOperation", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value); - - // Unwrap raw data to a string - foreach(var n in dt) - try { n["Data"] = System.Text.Encoding.UTF8.GetString((byte[])n["Data"]); } - catch { } - - info.OutputOK(dt); - } - } - private void IsDBUsedElseWhere(IBackup backup, RequestInfo info) - { - info.OutputOK(new { inuse = Library.Main.DatabaseLocator.IsDatabasePathInUse(backup.DBPath) }); - } - - public static void RemovePasswords(IBackup backup) - { - backup.SanitizeSettings(); - backup.SanitizeTargetUrl(); - } - - private void Export(IBackup backup, RequestInfo info) - { - var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false); - var argsonly = Library.Utility.Utility.ParseBool(info.Request.QueryString["argsonly"].Value, false); - var exportPasswords = Library.Utility.Utility.ParseBool(info.Request.QueryString["export-passwords"].Value, false); - if (!exportPasswords) - { - Backup.RemovePasswords(backup); - } - - if (cmdline) - { - info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) }); - } - else if (argsonly) - { - var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup)); - - info.OutputOK(new { - Backend = parts.First(), - Arguments = parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)), - Options = parts.Skip(1).Where(x => x.StartsWith("--", StringComparison.Ordinal)) - }); - } - else - { - var passphrase = info.Request.QueryString["passphrase"].Value; - byte[] data = Backup.ExportToJSON(backup, passphrase); - - string filename = Library.Utility.Uri.UrlEncode(backup.Name) + "-duplicati-config.json"; - if (!string.IsNullOrWhiteSpace(passphrase)) - { - filename += ".aes"; - } - - info.Response.ContentLength = data.Length; - info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename)); - info.Response.ContentType = "application/octet-stream"; - - info.BodyWriter.SetOK(); - info.Response.SendHeaders(); - info.Response.SendBody(data); - } - } - - public static byte[] ExportToJSON(IBackup backup, string passphrase) - { - Serializable.ImportExportStructure ipx = FIXMEGlobal.DataConnection.PrepareBackupForExport(backup); - - byte[] data; - using (MemoryStream ms = new System.IO.MemoryStream()) - { - using (StreamWriter sw = new System.IO.StreamWriter(ms)) - { - Serializer.SerializeJson(sw, ipx, true); - - if (!string.IsNullOrWhiteSpace(passphrase)) - { - ms.Position = 0; - using (MemoryStream ms2 = new System.IO.MemoryStream()) - { - using (Library.Encryption.AESEncryption m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary())) - { - m.Encrypt(ms, ms2); - data = ms2.ToArray(); - } - } - } - else - { - data = ms.ToArray(); - } - } - } - - return data; - } - - private void RestoreFiles(IBackup backup, RequestInfo info) - { - var input = info.Request.Form; - - string[] filters = parsePaths(input["paths"].Value ?? string.Empty); - - var passphrase = string.IsNullOrEmpty(input["passphrase"].Value) ? null : input["passphrase"].Value; - - var time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(input["time"].Value, DateTime.Now); - var restoreTarget = input["restore-path"].Value; - var overwrite = Duplicati.Library.Utility.Utility.ParseBool(input["overwrite"].Value, false); - - var permissions = Duplicati.Library.Utility.Utility.ParseBool(input["permissions"].Value, false); - var skip_metadata = Duplicati.Library.Utility.Utility.ParseBool(input["skip-metadata"].Value, false); - - var task = Runner.CreateRestoreTask(backup, filters, time, restoreTarget, overwrite, permissions, skip_metadata, passphrase); - - FIXMEGlobal.WorkThread.AddTask(task); - - info.OutputOK(new { TaskID = task.TaskID }); - } - - private void CreateReport(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.CreateReport, backup); - FIXMEGlobal.WorkThread.AddTask(task); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private void ReportRemoteSize(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.ListRemote, backup); - FIXMEGlobal.WorkThread.AddTask(task); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private void Repair(IBackup backup, RequestInfo info) - { - DoRepair(backup, info, false); - } - - private void RepairUpdate(IBackup backup, RequestInfo info) - { - DoRepair(backup, info, true); - } - - private void Vacuum(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.Vacuum, backup); - FIXMEGlobal.WorkThread.AddTask(task); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private void Verify(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.Verify, backup); - FIXMEGlobal.WorkThread.AddTask(task); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new {Status = "OK", ID = task.TaskID}); - } - - private void Compact(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.Compact, backup); - FIXMEGlobal.WorkThread.AddTask(task); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private string[] parsePaths(string paths) - { - string[] filters; - var rawpaths = (paths ?? string.Empty).Trim(); - - // We send the file list as a JSON array to avoid encoding issues with the path separator - // as it is an allowed character in file and path names. - // We also accept the old way, for compatibility with the greeno theme - if (!string.IsNullOrWhiteSpace(rawpaths) && rawpaths.StartsWith("[", StringComparison.Ordinal) && rawpaths.EndsWith("]", StringComparison.Ordinal)) - filters = Newtonsoft.Json.JsonConvert.DeserializeObject(rawpaths); - else - filters = paths.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries); - - return filters; - } - - private void DoRepair(IBackup backup, RequestInfo info, bool repairUpdate) - { - var input = info.Request.Form; - string[] filters = null; - var extra = new Dictionary(); - if (input["only-paths"].Value != null) - extra["repair-only-paths"] = (Library.Utility.Utility.ParseBool(input["only-paths"].Value, false)).ToString(); - if (input["time"].Value != null) - extra["time"] = input["time"].Value; - if (input["version"].Value != null) - extra["version"] = input["version"].Value; - if (input["paths"].Value != null) - filters = parsePaths(input["paths"].Value); - - var task = Runner.CreateTask(repairUpdate ? DuplicatiOperation.RepairUpdate : DuplicatiOperation.Repair, backup, extra, filters); - FIXMEGlobal.WorkThread.AddTask(task); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new {Status = "OK", ID = task.TaskID}); - } - - private void RunBackup(IBackup backup, RequestInfo info) - { - var t = FIXMEGlobal.WorkThread.CurrentTask; - var bt = t == null ? null : t.Backup; - if (bt != null && backup.ID == bt.ID) - { - // Already running - } - else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x => { - var bn = x?.Backup; - return bn == null || bn.ID == backup.ID; - })) - { - // Already in queue - } - else - { - FIXMEGlobal.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - } - - info.OutputOK(); - } - - private void IsActive(IBackup backup, RequestInfo info) - { - var t = FIXMEGlobal.WorkThread.CurrentTask; - var bt = t?.Backup; - if (bt != null && backup.ID == bt.ID) - { - info.OutputOK(new { Status = "OK", Active = true }); - return; - } - else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x => - { - var bn = x?.Backup; - return bn == null || bn.ID == backup.ID; - })) - { - info.OutputOK(new { Status = "OK", Active = true }); - return; - } - else - { - info.OutputOK(new { Status = "OK", Active = false }); - return; - } - } - - private void UpdateDatabasePath(IBackup backup, RequestInfo info, bool move) - { - var np = info.Request.Form["path"].Value; - if (string.IsNullOrWhiteSpace(np)) - info.ReportClientError("No target path supplied", System.Net.HttpStatusCode.BadRequest); - else if (!Path.IsPathRooted(np)) - info.ReportClientError("Target path is relative, please supply a fully qualified path", System.Net.HttpStatusCode.BadRequest); - else - { - if (move && (File.Exists(np) || Directory.Exists(np))) - info.ReportClientError("A file already exists at the new location", System.Net.HttpStatusCode.Conflict); - else - { - if (move) - File.Move(backup.DBPath, np); - - FIXMEGlobal.DataConnection.UpdateBackupDBPath(backup, np); - } - - } - - } - - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First()); - if (bk == null) - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); - else - { - if (parts.Length > 1) - { - var operation = parts.Last().Split(new char[] {'/'}).First().ToLowerInvariant(); - - switch (operation) - { - case "files": - var filter = parts.Last().Split(new char[] { '/' }, 2).Skip(1).FirstOrDefault(); - if (!string.IsNullOrWhiteSpace(info.Request.QueryString["filter"].Value)) - filter = info.Request.QueryString["filter"].Value; - SearchFiles(bk, filter, info); - return; - case "log": - FetchLogData(bk, info); - return; - case "remotelog": - FetchRemoteLogData(bk, info); - return; - case "filesets": - ListFileSets(bk, info); - return; - case "export": - Export(bk, info); - return; - case "isdbusedelsewhere": - IsDBUsedElseWhere(bk, info); - return; - case "isactive": - IsActive(bk, info); - return; - default: - info.ReportClientError(string.Format("Invalid component: {0}", operation), System.Net.HttpStatusCode.BadRequest); - return; - } - - } - - var scheduleId = FIXMEGlobal.DataConnection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID }); - var schedule = scheduleId.Any() ? FIXMEGlobal.DataConnection.GetSchedule(scheduleId.First()) : null; - var sourcenames = SpecialFolders.GetSourceNames(bk); - - //TODO: Filter out the password in both settings and the target url - - info.OutputOK(new GetResponse() - { - success = true, - data = new GetResponse.GetResponseData { - Schedule = schedule, - Backup = bk, - DisplayNames = sourcenames - } - }); - } - } - - public void POST(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First()); - if (bk == null) - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); - else - { - if (parts.Length > 1) - { - var operation = parts.Last().Split(new char[] { '/' }).First().ToLowerInvariant(); - - switch (operation) - { - case "deletedb": - System.IO.File.Delete(bk.DBPath); - info.OutputOK(); - return; - - case "movedb": - UpdateDatabasePath(bk, info, true); - return; - - case "updatedb": - UpdateDatabasePath(bk, info, false); - return; - - case "restore": - RestoreFiles(bk, info); - return; - - case "createreport": - CreateReport(bk, info); - return; - - case "repair": - Repair(bk, info); - return; - - case "repairupdate": - RepairUpdate(bk, info); - return; - - case "vacuum": - Vacuum(bk, info); - return; - - case "verify": - Verify(bk, info); - return; - - case "compact": - Compact(bk, info); - return; - - case "start": - case "run": - RunBackup(bk, info); - return; - - case "report-remote-size": - ReportRemoteSize(bk, info); - return; - - case "copytotemp": - var ipx = Serializer.Deserialize(new StringReader(Newtonsoft.Json.JsonConvert.SerializeObject(bk))); - - using(var tf = new Duplicati.Library.Utility.TempFile()) - ipx.DBPath = tf; - ipx.ID = null; - - info.OutputOK(new { status = "OK", ID = FIXMEGlobal.DataConnection.RegisterTemporaryBackup(ipx) }); - return; - } - } - - info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest); - } - } - - public void PUT(string key, RequestInfo info) - { - string str = info.Request.Form["data"].Value; - if (string.IsNullOrWhiteSpace(str)) - str = System.Threading.Tasks.Task.Run(async () => - { - using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) - - return await sr.ReadToEndAsync(); - }).GetAwaiter().GetResult(); - - if (string.IsNullOrWhiteSpace(str)) - { - info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest); - return; - } - - Backups.AddOrUpdateBackupData data = null; - try - { - data = Serializer.Deserialize(new StringReader(str)); - if (data.Backup == null) - { - info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest); - return; - } - - if (!string.IsNullOrEmpty(key)) - data.Backup.ID = key; - - if (string.IsNullOrEmpty(data.Backup.ID)) - { - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.BadRequest); - return; - } - - - if (data.Backup.IsTemporary) - { - var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID); - if (backup.IsTemporary) - throw new InvalidDataException("External is temporary but internal is not?"); - - FIXMEGlobal.DataConnection.UpdateTemporaryBackup(backup); - info.OutputOK(); - } - else - { - lock(FIXMEGlobal.DataConnection.m_lock) - { - var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID); - if (backup == null) - { - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); - return; - } - - if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase) && x.ID != data.Backup.ID)) - { - info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict); - return; - } - - var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule); - if (!string.IsNullOrWhiteSpace(err)) - { - info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); - return; - } - - //TODO: Merge in real passwords where the placeholder is found - FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); - - } - - info.OutputOK(); - } - } - catch (Exception ex) - { - if (data == null) - info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); - else - info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); - } - } - - public void DELETE(string key, RequestInfo info) - { - var backup = FIXMEGlobal.DataConnection.GetBackup(key); - if (backup == null) - { - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); - return; - } - - var delete_remote_files = info.Request is IModernHttpRequestAccess request && Library.Utility.Utility.ParseBool(request.GetQueryParam("delete-remote-files"), false); - - if (delete_remote_files) - { - var captcha_token = info.Request.Param["captcha-token"].Value; - var captcha_answer = info.Request.Param["captcha-answer"].Value; - if (string.IsNullOrWhiteSpace(captcha_token) || string.IsNullOrWhiteSpace(captcha_answer)) - { - info.ReportClientError("Missing captcha", System.Net.HttpStatusCode.Unauthorized); - return; - } - - if (!Captcha.SolvedCaptcha(captcha_token, "DELETE /backup/" + backup.ID, captcha_answer)) - { - info.ReportClientError("Invalid captcha", System.Net.HttpStatusCode.Forbidden); - return; - } - } - - if (FIXMEGlobal.WorkThread.Active) - { - try - { - //TODO: It's not safe to access the values like this, - //because the runner thread might interfere - var nt = FIXMEGlobal.WorkThread.CurrentTask; - if (backup.Equals(nt == null ? null : nt.Backup)) - { - bool force; - if (!bool.TryParse(info.Request.QueryString["force"].Value, out force)) - force = false; - - if (!force) - { - info.OutputError(new { status = "failed", reason = "backup-in-progress" }); - return; - } - - bool hasPaused = FIXMEGlobal.LiveControl.State == LiveControls.LiveControlState.Paused; - FIXMEGlobal.LiveControl.Pause(); - - for(int i = 0; i < 10; i++) - if (FIXMEGlobal.WorkThread.Active) - { - var t = FIXMEGlobal.WorkThread.CurrentTask; - if (backup.Equals(t == null ? null : t.Backup)) - System.Threading.Thread.Sleep(1000); - else - break; - } - else - break; - - if (FIXMEGlobal.WorkThread.Active) - { - var t = FIXMEGlobal.WorkThread.CurrentTask; - if (backup.Equals(t == null ? null : t.Backup)) - { - if (hasPaused) - FIXMEGlobal.LiveControl.Resume(); - info.OutputError(new { status = "failed", reason = "backup-unstoppable" }); - return; - } - } - - if (hasPaused) - FIXMEGlobal.LiveControl.Resume(); - } - } - catch (Exception ex) - { - info.OutputError(new { status = "error", message = ex.Message }); - return; - } - } - - var extra = new Dictionary(); - if (!string.IsNullOrWhiteSpace(info.Request.Param["delete-local-db"].Value)) - extra["delete-local-db"] = info.Request.Param["delete-local-db"].Value; - if (delete_remote_files) - extra["delete-remote-files"] = "true"; - - var task = Runner.CreateTask(DuplicatiOperation.Delete, backup, extra); - FIXMEGlobal.WorkThread.AddTask(task); - FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - public string Description { get { return "Retrieves, updates or deletes an existing backup and schedule"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), - new KeyValuePair(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)), - new KeyValuePair(HttpServer.Method.Delete, typeof(long)) - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs deleted file mode 100644 index 4d6675520..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs +++ /dev/null @@ -1,141 +0,0 @@ -// 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; -using System.Linq; -using Duplicati.Library.Common.IO; -using Duplicati.Library.RestAPI; -using Duplicati.Library.Utility; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class BackupDefaults : IRESTMethodGET - { - - private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(); - - public void GET(string key, RequestInfo info) - { - // Start with a scratch object - var o = new Newtonsoft.Json.Linq.JObject(); - - // Add application wide settings - o.Add("ApplicationOptions", new Newtonsoft.Json.Linq.JArray( - from n in FIXMEGlobal.DataConnection.Settings - select Newtonsoft.Json.Linq.JObject.FromObject(n) - )); - - try - { - // Add built-in defaults - Newtonsoft.Json.Linq.JObject n; - using(var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(FIXMEGlobal), "newbackup.json"))) - n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd()); - - MergeJsonObjects(o, n); - } - catch (Exception e) - { - Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to locate embeded backup defaults"); - } - - try - { - // Add install defaults/overrides, if present - var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, "newbackup.json"); - if (System.IO.File.Exists(path)) - { - Newtonsoft.Json.Linq.JObject n; - n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(path)); - - MergeJsonObjects(o, n); - } - } - catch (Exception e) - { - Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to process newbackup.json"); - } - - info.OutputOK(new - { - success = true, - data = o - }); - } - - private static void MergeJsonObjects(Newtonsoft.Json.Linq.JObject self, Newtonsoft.Json.Linq.JObject other) - { - foreach(var p in other.Properties()) - { - var sp = self.Property(p.Name); - if (sp == null) - self.Add(p); - else - { - switch (p.Type) - { - // Primitives override - case Newtonsoft.Json.Linq.JTokenType.Boolean: - case Newtonsoft.Json.Linq.JTokenType.Bytes: - case Newtonsoft.Json.Linq.JTokenType.Comment: - case Newtonsoft.Json.Linq.JTokenType.Constructor: - case Newtonsoft.Json.Linq.JTokenType.Date: - case Newtonsoft.Json.Linq.JTokenType.Float: - case Newtonsoft.Json.Linq.JTokenType.Guid: - case Newtonsoft.Json.Linq.JTokenType.Integer: - case Newtonsoft.Json.Linq.JTokenType.String: - case Newtonsoft.Json.Linq.JTokenType.TimeSpan: - case Newtonsoft.Json.Linq.JTokenType.Uri: - case Newtonsoft.Json.Linq.JTokenType.None: - case Newtonsoft.Json.Linq.JTokenType.Null: - case Newtonsoft.Json.Linq.JTokenType.Undefined: - self.Replace(p); - break; - - // Arrays merge - case Newtonsoft.Json.Linq.JTokenType.Array: - if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Array) - sp.Value = new Newtonsoft.Json.Linq.JArray(((Newtonsoft.Json.Linq.JArray)sp.Value).Union((Newtonsoft.Json.Linq.JArray)p.Value)); - else - { - var a = new Newtonsoft.Json.Linq.JArray(sp.Value); - sp.Value = new Newtonsoft.Json.Linq.JArray(a.Union((Newtonsoft.Json.Linq.JArray)p.Value)); - } - - break; - - // Objects merge - case Newtonsoft.Json.Linq.JTokenType.Object: - if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Object) - MergeJsonObjects((Newtonsoft.Json.Linq.JObject)sp.Value, (Newtonsoft.Json.Linq.JObject)p.Value); - else - sp.Value = p.Value; - break; - - // Ignore other stuff - default: - break; - } - } - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Backups.cs b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs deleted file mode 100644 index 2c06f365d..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Backups.cs +++ /dev/null @@ -1,310 +0,0 @@ -// 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; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Server.Serialization; -using System.IO; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Backups : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented - { - public class AddOrUpdateBackupData - { - public Boolean IsUnencryptedOrPassphraseStored { get; set;} - public Database.Schedule Schedule { get; set;} - public Database.Backup Backup { get; set;} - } - - public void GET(string key, RequestInfo info) - { - var schedules = FIXMEGlobal.DataConnection.Schedules; - var backups = FIXMEGlobal.DataConnection.Backups; - - var all = from n in backups - select new AddOrUpdateBackupData { - IsUnencryptedOrPassphraseStored = FIXMEGlobal.DataConnection.IsUnencryptedOrPassphraseStored(long.Parse(n.ID)), - Backup = (Database.Backup)n, - Schedule = - (from x in schedules - where x.Tags != null && x.Tags.Contains("ID=" + n.ID) - select (Database.Schedule)x).FirstOrDefault() - }; - - info.BodyWriter.OutputOK(all.ToArray()); - } - - private void ImportBackup(RequestInfo info) - { - var output_template = ""; - //output_template = ""; - try - { - var input = info.Request.Form; - var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false); - var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false); - var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false); - output_template = output_template.Replace("CBM", input["callback"].Value); - if (cmdline) - { - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", "Import from commandline not yet implemented")); - } - else - { - var file = info.Request.Form.GetFile("config"); - if (file == null) - throw new Exception("No file uploaded"); - - Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value); - if (direct) - { - lock (FIXMEGlobal.DataConnection.m_lock) - { - var basename = ipx.Backup.Name; - var c = 0; - while (c++ < 100 && FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) - ipx.Backup.Name = basename + " (" + c.ToString() + ")"; - - if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) - { - info.BodyWriter.SetOK(); - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", "There already exists a backup with the name: " + basename.Replace("\'", "\\'"))); - } - - var err = FIXMEGlobal.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule); - if (!string.IsNullOrWhiteSpace(err)) - { - info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); - return; - } - - FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule); - } - - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", "OK")); - - } - else - { - using (var sw = new StringWriter()) - { - Serializer.SerializeJson(sw, ipx, true); - output_template = output_template.Replace("'JSO'", sw.ToString()); - } - info.BodyWriter.Write(output_template.Replace("MSG", "Import completed, but a browser issue prevents loading the contents. Try using the direct import method instead.")); - } - } - } - catch (Exception ex) - { - FIXMEGlobal.DataConnection.LogError("", "Failed to import backup", ex); - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", ex.Message.Replace("\'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n"))); - } - } - - public static Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func getPassword, Dictionary advancedOptions) - { - // This removes the ID and DBPath from the backup configuration. - Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword); - - // This will create the Duplicati-server.sqlite database file if it doesn't exist. - using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(null, advancedOptions)) - { - if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase))) - { - throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists."); - } - - string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule); - if (!string.IsNullOrWhiteSpace(error)) - { - throw new InvalidOperationException(error); - } - - // This creates a new ID and DBPath. - connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule); - } - - return importedStructure; - } - - private static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func getPassword) - { - Serializable.ImportExportStructure ipx; - - var buf = new byte[3]; - using (var fs = System.IO.File.OpenRead(filename)) - { - Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length); - - fs.Position = 0; - if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S') - { - using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary())) - { - using (var m2 = m.Decrypt(fs)) - { - using (var sr = new System.IO.StreamReader(m2)) - { - ipx = Serializer.Deserialize(sr); - } - } - } - } - else - { - using (var sr = new System.IO.StreamReader(fs)) - { - ipx = Serializer.Deserialize(sr); - } - } - } - - if (ipx.Backup == null) - { - throw new Exception("No backup found in document"); - } - - if (ipx.Backup.Metadata == null) - { - ipx.Backup.Metadata = new Dictionary(); - } - - if (!importMetadata) - { - ipx.Backup.Metadata.Clear(); - } - - ipx.Backup.ID = null; - ipx.Backup.DBPath = null; - - if (ipx.Schedule != null) - { - ipx.Schedule.ID = -1; - } - - return ipx; - } - - public void POST(string key, RequestInfo info) - { - if ("import".Equals(key, StringComparison.OrdinalIgnoreCase)) - { - ImportBackup(info); - return; - } - - AddOrUpdateBackupData data = null; - try - { - var str = info.Request.Form["data"].Value; - if (string.IsNullOrWhiteSpace(str)) { - str = System.Threading.Tasks.Task.Run(async () => - { - using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) - - return await sr.ReadToEndAsync(); - }).GetAwaiter().GetResult(); - } - - data = Serializer.Deserialize(new StringReader(str)); - if (data.Backup == null) - { - info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest); - return; - } - - data.Backup.ID = null; - - if (Duplicati.Library.Utility.Utility.ParseBool(info.Request.Form["temporary"].Value, false)) - { - using(var tf = new Duplicati.Library.Utility.TempFile()) - data.Backup.DBPath = tf; - - data.Backup.Filters = data.Backup.Filters ?? new Duplicati.Server.Serialization.Interface.IFilter[0]; - data.Backup.Settings = data.Backup.Settings ?? new Duplicati.Server.Serialization.Interface.ISetting[0]; - - FIXMEGlobal.DataConnection.RegisterTemporaryBackup(data.Backup); - - info.OutputOK(new { status = "OK", ID = data.Backup.ID }); - } - else - { - if (Library.Utility.Utility.ParseBool(info.Request.Form["existing_db"].Value, false)) - { - data.Backup.DBPath = Library.Main.DatabaseLocator.GetDatabasePath(data.Backup.TargetURL, null, false, false); - if (string.IsNullOrWhiteSpace(data.Backup.DBPath)) - throw new Exception("Unable to find remote db path?"); - } - - - lock(FIXMEGlobal.DataConnection.m_lock) - { - if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase))) - { - info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict); - return; - } - - var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule); - if (!string.IsNullOrWhiteSpace(err)) - { - info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); - return; - } - - FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); - } - - info.OutputOK(new { status = "OK", ID = data.Backup.ID }); - } - } - catch (Exception ex) - { - if (data == null) - info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); - else - info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); - } - } - - - public string Description { get { return "Return a list of current backups and their schedules"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])), - new KeyValuePair(HttpServer.Method.Post, typeof(AddOrUpdateBackupData)) - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs b/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs deleted file mode 100644 index 388a32aad..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs +++ /dev/null @@ -1,62 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class BugReport : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - long id; - long.TryParse(key, out id); - - var tf = FIXMEGlobal.DataConnection.GetTempFiles().FirstOrDefault(x => x.ID == id); - if (tf == null) - { - info.ReportClientError("Invalid or missing bugreport id", System.Net.HttpStatusCode.NotFound); - return; - } - - if (!System.IO.File.Exists(tf.Path)) - { - info.ReportClientError("File is missing", System.Net.HttpStatusCode.NotFound); - return; - } - - var filename = "bugreport.zip"; - using(var fs = System.IO.File.OpenRead(tf.Path)) - { - info.Response.ContentLength = fs.Length; - info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename)); - info.Response.ContentType = "application/octet-stream"; - - info.BodyWriter.SetOK(); - info.Response.SendHeaders(); - fs.CopyTo(info.Response.Body); - info.Response.Send(); - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs b/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs deleted file mode 100644 index 93345c8f8..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs +++ /dev/null @@ -1,172 +0,0 @@ -// 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; -using System.Linq; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Captcha : IRESTMethodGET, IRESTMethodPOST - { - private class CaptchaEntry - { - public readonly string Answer; - public readonly string Target; - public int Attempts; - public readonly DateTime Expires; - - public CaptchaEntry(string answer, string target) - { - Answer = answer; - Target = target; - Attempts = 4; - Expires = DateTime.Now.AddMinutes(2); - } - } - - private static readonly object m_lock = new object(); - private static readonly Dictionary m_captchas = new Dictionary(); - - public static bool SolvedCaptcha(string token, string target, string answer) - { - lock(m_lock) - { - CaptchaEntry tp; - m_captchas.TryGetValue(token ?? string.Empty, out tp); - if (tp == null) - return false; - - if (tp.Attempts > 0) - tp.Attempts--; - - return tp.Attempts >= 0 && string.Equals(tp.Answer, answer, StringComparison.OrdinalIgnoreCase) && tp.Target == target && tp.Expires >= DateTime.Now; - } - } - - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized); - return; - } - else - { - string answer = null; - lock (m_lock) - { - CaptchaEntry tp; - m_captchas.TryGetValue(key, out tp); - if (tp != null && tp.Expires > DateTime.Now) - answer = tp.Answer; - } - - if (string.IsNullOrWhiteSpace(answer)) - { - info.ReportClientError("No such entry", System.Net.HttpStatusCode.NotFound); - return; - } - - using (var bmp = CaptchaUtil.CreateCaptcha(answer)) - using (var ms = new System.IO.MemoryStream()) - { - info.Response.ContentType = "image/jpeg"; - info.Response.ContentLength = ms.Length; - bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); - ms.Position = 0; - - info.Response.ContentType = "image/jpeg"; - info.Response.ContentLength = ms.Length; - info.Response.SendHeaders(); - ms.CopyTo(info.Response.Body); - info.Response.Send(); - } - } - } - - public void POST(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - var target = info.Request.Param["target"].Value; - if (string.IsNullOrWhiteSpace(target)) - { - info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); - return; - } - - var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6); - var nonce = Guid.NewGuid().ToString(); - - string token; - using (var ms = new System.IO.MemoryStream()) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(answer + nonce); - ms.Write(bytes, 0, bytes.Length); - ms.Position = 0; - using(var hasher = Library.Utility.HashFactory.CreateHasher(Library.Utility.HashFactory.SHA256)){ - token = Library.Utility.Utility.Base64PlainToBase64Url(Convert.ToBase64String(hasher.ComputeHash(ms))); - } - } - - lock (m_lock) - { - var expired = m_captchas.Where(x => x.Value.Expires < DateTime.Now).Select(x => x.Key).ToArray(); - foreach (var x in expired) - m_captchas.Remove(x); - - if (m_captchas.Count > 3) - { - info.ReportClientError("Too many captchas, wait 2 minutes and try again", System.Net.HttpStatusCode.ServiceUnavailable); - return; - } - - m_captchas[token] = new CaptchaEntry(answer, target); - } - - info.OutputOK(new - { - token = token - }); - } - else - { - var answer = info.Request.Param["answer"].Value; - var target = info.Request.Param["target"].Value; - if (string.IsNullOrWhiteSpace(answer)) - { - info.ReportClientError("Missing answer parameter", System.Net.HttpStatusCode.BadRequest); - return; - } - if (string.IsNullOrWhiteSpace(target)) - { - info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); - return; - } - - if (SolvedCaptcha(key, target, answer)) - info.OutputOK(); - else - info.ReportClientError("Incorrect", System.Net.HttpStatusCode.Forbidden); - } - } - } -} diff --git a/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs b/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs deleted file mode 100644 index 7a9712562..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs +++ /dev/null @@ -1,79 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Changelog : IRESTMethodGET, IRESTMethodDocumented - { - private class GetResponse - { - public string Status; - public string Version; - public string Changelog; - } - - public void GET(string key, RequestInfo info) - { - var fromUpdate = info.Request.QueryString["from-update"].Value; - if (!Library.Utility.Utility.ParseBool(fromUpdate, false)) - { - var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "changelog.txt"); - info.OutputOK(new GetResponse() { - Status = "OK", - Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), - Changelog = System.IO.File.ReadAllText(path) - }); - } - else - { - var updateInfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion; - if (updateInfo == null) - { - info.ReportClientError("No update found", System.Net.HttpStatusCode.NotFound); - } - else - { - info.OutputOK(new GetResponse() { - Status = "OK", - Version = updateInfo.Version, - Changelog = updateInfo.ChangeInfo - }); - } - } - } - - public string Description { get { return "Gets the current changelog"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs b/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs deleted file mode 100644 index e4ff87db9..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs +++ /dev/null @@ -1,293 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class CommandLine : IRESTMethodGET, IRESTMethodPOST - { - private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(); - - private class LogWriter : System.IO.TextWriter - { - private readonly ActiveRun m_target; - private readonly StringBuilder m_sb = new StringBuilder(); - private int m_newlinechars = 0; - - public LogWriter(ActiveRun target) - { - m_target = target; - } - - public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } } - - public override void Write(char value) - { - lock(m_target.Lock) - { - m_sb.Append(value); - if (NewLine[m_newlinechars] == value) - { - m_newlinechars++; - if (m_newlinechars == NewLine.Length) - WriteLine(string.Empty); - } - else - m_newlinechars = 0; - } - } - - public override void WriteLine(string value) - { - value = value ?? string.Empty; - lock(m_target.Lock) - { - m_target.LastAccess = DateTime.Now; - - //Avoid writing the log if it does not exist - if (m_target.IsLogDisposed) - { - FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null)); - return; - } - - try - { - if (m_sb.Length != 0) - { - m_target.Log.Add(m_sb + value); - m_sb.Length = 0; - m_newlinechars = 0; - } - else - { - m_target.Log.Add(value); - } - } - catch (Exception ex) - { - // This can happen on a very unlucky race where IsLogDisposed is set right after the check - FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex)); - } - } - } - } - - private class ActiveRun - { - public readonly string ID = Guid.NewGuid().ToString(); - public DateTime LastAccess = DateTime.Now; - public readonly Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList(); - public Runner.IRunnerData Task; - public LogWriter Writer; - public readonly object Lock = new object(); - public bool Finished = false; - public bool Started = false; - public bool IsLogDisposed = false; - public System.Threading.Thread Thread; - } - - private readonly Dictionary m_activeItems = new Dictionary(); - private System.Threading.Tasks.Task m_cleanupTask; - - public void POST(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - string[] args; - var str = System.Threading.Tasks.Task.Run(async () => - { - using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) - - return await sr.ReadToEndAsync(); - }).GetAwaiter().GetResult(); - args = Newtonsoft.Json.JsonConvert.DeserializeObject(str); - - var k = new ActiveRun(); - k.Writer = new LogWriter(k); - - m_activeItems[k.ID] = k; - StartCleanupTask(); - - k.Task = Runner.CreateCustomTask((sink) => - { - try - { - k.Thread = System.Threading.Thread.CurrentThread; - k.Started = true; - - var code = Duplicati.CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c => { - k.Task.SetController(c); - c.AppendSink(sink); - }, args); - k.Writer.WriteLine("Return code: {0}", code); - } - catch (Exception ex) - { - var rx = ex; - if (rx is System.Reflection.TargetInvocationException) - rx = rx.InnerException; - - if (rx is Library.Interface.UserInformationException) - k.Log.Add(rx.Message); - else - k.Log.Add(rx.ToString()); - - throw rx; - } - finally - { - k.Finished = true; - k.Thread = null; - } - }); - - FIXMEGlobal.WorkThread.AddTask(k.Task); - - info.OutputOK(new - { - ID = k.ID - }); - } - else - { - if (!key.EndsWith("/abort", StringComparison.OrdinalIgnoreCase)) - { - info.ReportClientError("Only abort commands are allowed", System.Net.HttpStatusCode.BadRequest); - return; - } - - key = key.Substring(0, key.Length - "/abort".Length); - if (string.IsNullOrWhiteSpace(key)) - { - info.ReportClientError("No task key found", System.Net.HttpStatusCode.BadRequest); - return; - } - - ActiveRun t; - if (!m_activeItems.TryGetValue(key, out t)) - { - info.OutputError(code: System.Net.HttpStatusCode.NotFound); - return; - } - - var tt = t.Task; - if (tt != null) - tt.Abort(); - - var tr = t.Thread; - if (tr != null) - tr.Interrupt(); - - info.OutputOK(); - } - } - - private void StartCleanupTask() - { - if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled) - m_cleanupTask = RunCleanupAsync(); - } - - private async System.Threading.Tasks.Task RunCleanupAsync() - { - while (m_activeItems.Count > 0) - { - var oldest = m_activeItems.Values - .OrderBy(x => x.LastAccess) - .FirstOrDefault(); - - if (oldest != null) - { - // If the task has finished, we just wait a little to allow the UI to pick it up - var timeout = oldest.Finished ? TimeSpan.FromMinutes(5) : TimeSpan.FromDays(1); - if (DateTime.Now - oldest.LastAccess > timeout) - { - oldest.IsLogDisposed = true; - m_activeItems.Remove(oldest.ID); - oldest.Log.Dispose(); - - // Fix all expired, or stop running - continue; - } - } - - await System.Threading.Tasks.Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false); - } - } - - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.OutputOK( - Duplicati.CommandLine.Program.SupportedCommands - ); - } - else - { - ActiveRun t; - if (!m_activeItems.TryGetValue(key, out t)) - { - info.OutputError(code: System.Net.HttpStatusCode.NotFound); - return; - } - - int pagesize; - int offset; - - int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize); - int.TryParse(info.Request.QueryString["offset"].Value, out offset); - pagesize = Math.Max(10, Math.Min(500, pagesize)); - offset = Math.Max(0, offset); - var items = new List(); - long count; - bool started; - bool finished; - - lock(t.Lock) - { - t.LastAccess = DateTime.Now; - count = t.Log.Count; - offset = Math.Min((int)count, offset); - items.AddRange(t.Log.Skip(offset).Take(pagesize)); - finished = t.Finished; - started = t.Started; - } - - info.OutputOK(new - { - Pagesize = pagesize, - Offset = offset, - Count = count, - Items = items, - Finished = finished, - Started = started - }); - } - } - } -} diff --git a/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs b/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs deleted file mode 100644 index 1b8c24490..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs +++ /dev/null @@ -1,296 +0,0 @@ -// 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; -using System.Collections.Generic; -using System.Linq; -using System.IO; -using Duplicati.Library.Snapshots; -using Duplicati.Library.Common.IO; -using Duplicati.Library.Common; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Filesystem : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }); - var path = Duplicati.Library.Utility.Uri.UrlDecode((parts.Length == 2 ? parts.FirstOrDefault() : key ?? "")); - var command = parts.Length == 2 ? parts.Last() : null; - if (string.IsNullOrEmpty(path)) - path = info.Request.QueryString["path"].Value; - - Process(command, path, info); - } - - private void Process(string command, string path, RequestInfo info) - { - if (string.IsNullOrEmpty(path)) - { - info.ReportClientError("No path parameter was found", System.Net.HttpStatusCode.BadRequest); - return; - } - - bool skipFiles = Library.Utility.Utility.ParseBool(info.Request.QueryString["onlyfolders"].Value, false); - bool showHidden = Library.Utility.Utility.ParseBool(info.Request.QueryString["showhidden"].Value, false); - - string specialpath = null; - string specialtoken = null; - - if (path.StartsWith("%", StringComparison.Ordinal)) - { - var ix = path.IndexOf("%", 1, StringComparison.Ordinal); - if (ix > 0) - { - var tk = path.Substring(0, ix + 1); - var node = SpecialFolders.Nodes.FirstOrDefault(x => x.id.Equals(tk, StringComparison.OrdinalIgnoreCase)); - if (node != null) - { - specialpath = node.resolvedpath; - specialtoken = node.id; - } - } - } - - path = SpecialFolders.ExpandEnvironmentVariables(path); - - if (Platform.IsClientPosix && !path.StartsWith("/", StringComparison.Ordinal)) - { - info.ReportClientError("The path parameter must start with a forward-slash", System.Net.HttpStatusCode.BadRequest); - return; - } - - if (!string.IsNullOrWhiteSpace(command)) - { - if ("validate".Equals(command, StringComparison.OrdinalIgnoreCase)) - { - try - { - if (System.IO.Path.IsPathRooted(path) && (System.IO.Directory.Exists(path) || System.IO.File.Exists(path))) - { - info.OutputOK(); - return; - } - } - catch - { - } - - info.ReportServerError("File or folder not found", System.Net.HttpStatusCode.NotFound); - return; - } - else - { - info.ReportClientError(string.Format("No such operation found: {0}", command), System.Net.HttpStatusCode.NotFound); - return; - } - } - - try - { - if (path != "" && path != "/") - path = Util.AppendDirSeparator(path); - - IEnumerable res; - - if (!Platform.IsClientPosix && (path.Equals("/") || path.Equals(""))) - { - res = DriveInfo.GetDrives() - .Where(di => - (di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable) - && di.IsReady // Only try to create TreeNode entries for drives who were ready 'now' - ) - .Select(TryCreateTreeNodeForDrive) // This will try to create a TreeNode for selected drives - .Where(tn => tn != null); // This filters out such entries that could not be created - } - else - { - res = ListFolderAsNodes(path, skipFiles, showHidden); - } - - if ((path.Equals("/") || path.Equals("")) && specialtoken == null) - { - // Prepend special folders - res = SpecialFolders.Nodes.Union(res); - } - - if (specialtoken != null) - { - res = res.Select(x => - { - x.resolvedpath = x.id; - x.id = specialtoken + x.id.Substring(specialpath.Length); - return x; - }); - } - - // We have to resolve the query before giving it to OutputOK - // If we do not do this, and the query throws an exception when OutputOK resolves it, - // the exception would not be handled properly - res = res.ToList(); - - info.OutputOK(res); - } - catch (Exception ex) - { - info.ReportClientError("Failed to process the path: " + ex.Message, System.Net.HttpStatusCode.InternalServerError); - } - } - - /// - /// Try to create a new TreeNode instance for the given DriveInfo instance. - /// - /// - /// If an exception occurs during creation (most likely the device became unavailable), a null is returned instead. - /// - /// - /// DriveInfo to try create a TreeNode for. Cannot be null. - /// A new TreeNode instance on success; null if an exception occurred during creation. - private static Serializable.TreeNode TryCreateTreeNodeForDrive(DriveInfo driveInfo) - { - if (driveInfo == null) throw new ArgumentNullException(nameof(driveInfo)); - - try - { - // Try to create the TreeNode - // This may still fail as the drive might become unavailable in the meanwhile - return new Serializable.TreeNode - { - id = driveInfo.RootDirectory.FullName, - text = - ( - string.IsNullOrWhiteSpace(driveInfo.VolumeLabel) - ? driveInfo.RootDirectory.FullName.Replace('\\', ' ') - : driveInfo.VolumeLabel + " - " + driveInfo.RootDirectory.FullName.Replace('\\', ' ') - ) + "(" + driveInfo.DriveType + ")", - iconCls = "x-tree-icon-drive" - }; - } - catch - { - // Drive became unavailable in the meanwhile or another exception occurred - // Return a null as fall back - return null; - } - } - - private static IEnumerable ListFolderAsNodes(string entrypath, bool skipFiles, bool showHidden) - { - //Helper function for finding out if a folder has sub elements - Func hasSubElements = (p) => skipFiles ? Directory.EnumerateDirectories(p).Any() : Directory.EnumerateFileSystemEntries(p).Any(); - - //Helper function for dealing with exceptions when accessing off-limits folders - Func isEmptyFolder = (p) => - { - try { return !hasSubElements(p); } - catch { } - return true; - }; - - //Helper function for dealing with exceptions when accessing off-limits folders - Func canAccess = (p) => - { - try { hasSubElements(p); return true; } - catch { } - return false; - }; - - Func getFileSize = (p) => - { - try { return new FileInfo(p).Length; } - catch { } - return -1; - }; - - foreach (var s in SystemIO.IO_OS.EnumerateFileSystemEntries(entrypath) - // Group directories first - .OrderByDescending(f => SystemIO.IO_OS.GetFileAttributes(f) & FileAttributes.Directory) - // Sort both groups (directories and files) alphabetically - .ThenBy(f => f)) - { - Serializable.TreeNode tn = null; - try - { - var attr = SystemIO.IO_OS.GetFileAttributes(s); - var isSymlink = SystemIO.IO_OS.IsSymlink(s, attr); - var isFolder = (attr & FileAttributes.Directory) != 0; - var isFile = !isFolder; - var isHidden = (attr & FileAttributes.Hidden) != 0; - bool isSystem = (attr & FileAttributes.System) != 0; - bool isTemporary = (attr & FileAttributes.Temporary) != 0; - long fileSize = -1; - - var accessible = isFile || canAccess(s); - var isLeaf = isFile || !accessible || isEmptyFolder(s); - - var rawid = isFolder ? Util.AppendDirSeparator(s) : s; - if (skipFiles && !isFolder) - continue; - - if (!showHidden && isHidden) - continue; - - if (isFile) - { - fileSize = getFileSize(s); - } - - tn = new Serializable.TreeNode() - { - id = rawid, - text = SystemIO.IO_OS.PathGetFileName(s), - hidden = isHidden, - symlink = isSymlink, - temporary = isTemporary, - systemFile = isSystem, - fileSize = fileSize, - iconCls = isFolder ? (accessible ? (isSymlink ? "x-tree-icon-symlink" : "x-tree-icon-parent") : "x-tree-icon-locked") : "x-tree-icon-leaf", - leaf = isLeaf - }; - } - catch - { - } - - if (tn != null) - yield return tn; - } - } - - public void POST(string key, RequestInfo info) - { - Process(key, info.Request.Form["path"].Value, info); - } - - public string Description { get { return "Enumerates the server filesystem"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(string[])), - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Help.cs b/Duplicati.Library.RestAPI/RESTMethods/Help.cs deleted file mode 100644 index b409b7f77..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Help.cs +++ /dev/null @@ -1,104 +0,0 @@ -// 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; -using System.Linq; -using System.Text; -using Newtonsoft.Json; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Help : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - var sb = new StringBuilder(); - if (string.IsNullOrWhiteSpace(key)) - { - foreach(var m in RESTHandler.Modules.Keys.OrderBy(x => x)) - { - var mod = RESTHandler.Modules[m]; - if (mod == this) - continue; - - var desc = mod.GetType().Name; - if (mod is IRESTMethodDocumented documented) - desc = documented.Description; - sb.AppendFormat(ITEM_TEMPLATE, RESTHandler.API_URI_PATH, m, mod.GetType().Name, desc); - } - - - var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, "API Information", "", sb)); - - info.Response.ContentType = "text/html"; - info.Response.ContentLength = data.Length; - info.Response.Body.Write(data, 0, data.Length); - info.Response.Send(); - } - else - { - IRESTMethod m; - RESTHandler.Modules.TryGetValue(key, out m); - if (m == null) - { - info.Response.Status = System.Net.HttpStatusCode.NotFound; - info.Response.Reason = "Module not found"; - } - else - { - var desc = ""; - if (m is IRESTMethodDocumented doc) - { - desc = doc.Description; - foreach(var t in doc.Types) - sb.AppendFormat(METHOD_TEMPLATE, t.Key, JsonConvert.SerializeObject(t.Value)); //TODO: Format the type - } - - var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, m.GetType().Name, desc, sb)); - - info.Response.ContentType = "text/html"; - info.Response.ContentLength = data.Length; - info.Response.Body.Write(data, 0, data.Length); - info.Response.Send(); - - } - } - } - - private const string TEMPLATE = @" -{0} - -

{0}

-

{1}

-
    -{2} -
- -"; - private const string ITEM_TEMPLATE = @" -
  • {2}: {3}
  • -"; - - private const string METHOD_TEMPLATE = @" -{0}:
    {1}
    -"; - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs b/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs deleted file mode 100644 index b957cfd48..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs +++ /dev/null @@ -1,93 +0,0 @@ -// 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; -using System.Collections.Generic; -using Duplicati.Library.Interface; -using System.Linq; -using System.Security.Principal; -using Duplicati.Library.Snapshots; -using Duplicati.Library.Common; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class HyperV : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - // Early exit in case we are non-windows to prevent attempting to load Windows-only components - if (Platform.IsClientWindows) - RealGET(key, info); - else - info.OutputOK(new string[0]); - } - - // Make sure the JIT does not attempt to inline this call and thus load - // referenced types from System.Management here - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private void RealGET(string key, RequestInfo info) - { - var hypervUtility = new HyperVUtility(); - - if (!hypervUtility.IsHyperVInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) - { - info.OutputOK(new string[0]); - return; - } - - try - { - if (string.IsNullOrEmpty(key)) - { - hypervUtility.QueryHyperVGuestsInfo(); - info.OutputOK(hypervUtility.Guests.Select(x => new { id = x.ID, name = x.Name }).ToList()); - } - else - { - hypervUtility.QueryHyperVGuestsInfo(true); - var foundVMs = hypervUtility.Guests.FindAll(x => x.ID.Equals(new Guid(key))); - - if (foundVMs.Count == 1) - info.OutputOK(foundVMs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList()); - else - info.ReportClientError(string.Format("Cannot find VM with ID {0}.", key), System.Net.HttpStatusCode.NotFound); - } - } - catch (Exception ex) - { - info.ReportServerError("Failed to enumerate Hyper-V virtual machines: " + ex.Message); - } - } - - public string Description { get { return "Return a list of Hyper-V virtual machines"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[])) - }; - } - } - - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs b/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs deleted file mode 100644 index 488f4ce1d..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs +++ /dev/null @@ -1,60 +0,0 @@ -// 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; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public interface IRESTMethod - { - } - - public interface IRESTMethodDocumented - { - string Description { get; } - IEnumerable> Types { get; } - } - - public interface IRESTMethodGET : IRESTMethod - { - void GET(string key, RequestInfo info); - } - public interface IRESTMethodPUT : IRESTMethod - { - void PUT(string key, RequestInfo info); - } - - public interface IRESTMethodPOST : IRESTMethod - { - void POST(string key, RequestInfo info); - } - - public interface IRESTMethodDELETE : IRESTMethod - { - void DELETE(string key, RequestInfo info); - } - - public interface IRESTMethodPATCH : IRESTMethod - { - void PATCH(string key, RequestInfo info); - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs b/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs deleted file mode 100644 index 39bd5d754..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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; -using System.IO; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Licenses : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - var exefolder = System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location); - var path = System.IO.Path.Combine(exefolder, "licenses"); - if (Duplicati.Library.Common.Platform.IsClientOSX && !Directory.Exists(path)) - { - // Go up one, as the licenses cannot be in the binary folder in MacOS Packages - exefolder = Path.GetDirectoryName(exefolder); - var test = Path.Combine(exefolder, "Licenses"); - if (Directory.Exists(test)) - path = test; - } - info.OutputOK(Duplicati.License.LicenseReader.ReadLicenses(path)); - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/LogData.cs b/Duplicati.Library.RestAPI/RESTMethods/LogData.cs deleted file mode 100644 index 1b78b4898..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/LogData.cs +++ /dev/null @@ -1,122 +0,0 @@ -// 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; -using System.Collections.Generic; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class LogData : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - if ("poll".Equals(key, StringComparison.OrdinalIgnoreCase)) - { - var input = info.Request.QueryString; - var level_str = input["level"].Value ?? ""; - var id_str = input["id"].Value ?? ""; - - int pagesize; - if (!int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize)) - pagesize = 100; - - pagesize = Math.Max(1, Math.Min(500, pagesize)); - - Library.Logging.LogMessageType level; - long id; - - long.TryParse(id_str, out id); - Enum.TryParse(level_str, true, out level); - - info.OutputOK(FIXMEGlobal.LogHandler.AfterID(id, level, pagesize)); - } - else - { - - List> res = null; - FIXMEGlobal.DataConnection.ExecuteWithCommand(x => - { - res = DumpTable(x, "ErrorLog", "Timestamp", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value); - }); - - info.OutputOK(res); - } - } - - - public static List> DumpTable(System.Data.IDbCommand cmd, string tablename, string pagingfield, string offset_str, string pagesize_str) - { - var result = new List>(); - - long pagesize; - if (!long.TryParse(pagesize_str, out pagesize)) - pagesize = 100; - - pagesize = Math.Max(10, Math.Min(500, pagesize)); - - cmd.CommandText = "SELECT * FROM \"" + tablename + "\""; - long offset = 0; - if (!string.IsNullOrWhiteSpace(offset_str) && long.TryParse(offset_str, out offset) && !string.IsNullOrEmpty(pagingfield)) - { - var p = cmd.CreateParameter(); - p.Value = offset; - cmd.Parameters.Add(p); - - cmd.CommandText += " WHERE \"" + pagingfield + "\" < ?"; - } - - if (!string.IsNullOrEmpty(pagingfield)) - cmd.CommandText += " ORDER BY \"" + pagingfield + "\" DESC"; - cmd.CommandText += " LIMIT " + pagesize.ToString(); - - using(var rd = cmd.ExecuteReader()) - { - var names = new List(); - for(var i = 0; i < rd.FieldCount; i++) - names.Add(rd.GetName(i)); - - while (rd.Read()) - { - var dict = new Dictionary(); - for(int i = 0; i < names.Count; i++) - dict[names[i]] = rd.GetValue(i); - - result.Add(dict); - } - } - - return result; - } - - public string Description { get { return "Retrieves system log data"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(Dictionary[])), - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs b/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs deleted file mode 100644 index be873efdb..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs +++ /dev/null @@ -1,91 +0,0 @@ -// 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; -using System.Collections.Generic; -using Duplicati.Library.Interface; -using System.Linq; -using System.Security.Principal; -using Duplicati.Library.Snapshots; -using Duplicati.Library.Common; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class MSSQL : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - // Early exit in case we are non-windows to prevent attempting to load Windows-only components - if (Platform.IsClientWindows) - RealGET(key, info); - else - info.OutputOK(new string[0]); - } - - // Make sure the JIT does not attempt to inline this call and thus load - // referenced types from System.Management here - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private void RealGET(string key, RequestInfo info) - { - var mssqlUtility = new MSSQLUtility(); - - if (!mssqlUtility.IsMSSQLInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) - { - info.OutputOK(new string[0]); - return; - } - - try - { - mssqlUtility.QueryDBsInfo(); - - if (string.IsNullOrEmpty(key)) - info.OutputOK(mssqlUtility.DBs.Select(x => new { id = x.ID, name = x.Name }).ToList()); - else - { - var foundDBs = mssqlUtility.DBs.FindAll(x => x.ID.Equals(key, StringComparison.OrdinalIgnoreCase)); - - if (foundDBs.Count == 1) - info.OutputOK(foundDBs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList()); - else - info.ReportClientError(string.Format("Cannot find DB with ID {0}.", key), System.Net.HttpStatusCode.NotFound); - } - } - catch (Exception ex) - { - info.ReportServerError("Failed to enumerate Microsoft SQL Server databases: " + ex.Message); - } - } - - public string Description { get { return "Return a list of Microsoft SQL Server databases"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[])) - }; - } - } - - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Notification.cs b/Duplicati.Library.RestAPI/RESTMethods/Notification.cs deleted file mode 100644 index 24199cf0b..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Notification.cs +++ /dev/null @@ -1,65 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Notification : IRESTMethodGET, IRESTMethodDELETE - { - public void GET(string key, RequestInfo info) - { - long id; - if (!long.TryParse(key, out id)) - { - info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest); - return; - } - - var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); - if (el == null) - info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); - else - info.OutputOK(el); - } - - public void DELETE(string key, RequestInfo info) - { - long id; - if (!long.TryParse(key, out id)) - { - info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest); - return; - } - - var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); - if (el == null) - info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); - else - { - FIXMEGlobal.DataConnection.DismissNotification(id); - info.OutputOK(); - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs b/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs deleted file mode 100644 index 3e4bb7b8b..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Notifications : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - info.OutputOK(FIXMEGlobal.DataConnection.GetNotifications()); - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs b/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs deleted file mode 100644 index e85974dd4..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs +++ /dev/null @@ -1,50 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class ProgressState : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - if (FIXMEGlobal.GenerateProgressState == null) - info.ReportClientError("No active backup", System.Net.HttpStatusCode.NotFound); - else - info.OutputOK(FIXMEGlobal.GenerateProgressState()); - } - - public string Description { get { return "Return the progress of the currently running operation."; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(Serialization.Interface.IProgressEventData)) - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs b/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs deleted file mode 100644 index 04997df72..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs +++ /dev/null @@ -1,265 +0,0 @@ -// 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; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Duplicati.Library.Interface; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class RemoteOperation : IRESTMethodGET, IRESTMethodPOST - { - private void LocateDbUri(string uri, RequestInfo info) - { - var path = Library.Main.DatabaseLocator.GetDatabasePath(uri, null, false, false); - info.OutputOK(new { - Exists = !string.IsNullOrWhiteSpace(path), - Path = path - }); - } - - private Dictionary ParseUrlOptions(Library.Utility.Uri uri) - { - var qp = uri.QueryParameters; - - var opts = Runner.GetCommonOptions(); - foreach (var k in qp.Keys.Cast()) - opts[k] = qp[k]; - - return opts; - } - - private IEnumerable ConfigureModules(IDictionary opts) - { - // TODO: This works because the generic modules are implemented - // with pre .NetCore logic, using static methods - // The modules are created to allow multipe dispose, - // which violates the .Net patterns - var modules = (from n in Library.DynamicLoader.GenericLoader.Modules - where n is Library.Interface.IConnectionModule - select n).ToArray(); - - foreach (var n in modules) - n.Configure(opts); - - return modules; - } - - - private class TupleDisposeWrapper : IDisposable - { - public IBackend Backend { get; set; } - public IEnumerable Modules { get; set; } - - public void Dispose() - { - Backend.Dispose(); - DisposeModules(); - } - - public void DisposeModules() - { - foreach (var n in Modules) - if (n is IDisposable disposable) - disposable.Dispose(); - } - } - - private TupleDisposeWrapper GetBackend(string url) - { - var uri = new Library.Utility.Uri(url); - var opts = ParseUrlOptions(uri); - var modules = ConfigureModules(opts); - var backend = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(url, new Dictionary()); - return new TupleDisposeWrapper() { Backend = backend, Modules = modules }; - } - - private void CreateFolder(string uri, RequestInfo info) - { - using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) - b.CreateFolder(); - - info.OutputOK(); - } - - private void UploadFile(string uri, RequestInfo info) - { - var data = info.Request.QueryString["data"].Value; - var remotename = info.Request.QueryString["filename"].Value; - - using(var ms = new System.IO.MemoryStream()) - using(var b = GetBackend(uri)) - { - using(var tf = new Library.Utility.TempFile()) - { - System.IO.File.WriteAllText(tf, data); - b.Backend.PutAsync(remotename, tf, CancellationToken.None).Wait(); - } - } - - info.OutputOK(); - } - - private void ListFolder(string uri, RequestInfo info) - { - using(var b = GetBackend(uri)) - info.OutputOK(b.Backend.List()); - } - - private void TestConnection(string url, RequestInfo info) - { - bool autoCreate = info.Request.Param.Contains("autocreate") - ? Library.Utility.Utility.ParseBool(info.Request.Param["autocreate"].Value, false) - : false; - - TupleDisposeWrapper wrapper = null; - - try - { - wrapper = GetBackend(url); - - using (var b = wrapper.Backend) - { - try { b.Test(); } - catch (FolderMissingException) - { - if (!autoCreate) - throw; - - b.CreateFolder(); - b.Test(); - } - info.OutputOK(); - } - } - catch (Duplicati.Library.Interface.FolderMissingException) - { - if (!autoCreate) { - info.ReportServerError("missing-folder"); - } else { - info.ReportServerError("error-creating-folder"); - } - } - catch (Duplicati.Library.Utility.SslCertificateValidator.InvalidCertificateException icex) - { - if (string.IsNullOrWhiteSpace(icex.Certificate)) - info.ReportServerError(icex.Message); - else - info.ReportServerError("incorrect-cert:" + icex.Certificate); - } - catch (Duplicati.Library.Utility.HostKeyException hex) - { - if (string.IsNullOrWhiteSpace(hex.ReportedHostKey)) - info.ReportServerError(hex.Message); - else - { - info.ReportServerError(string.Format( - @"incorrect-host-key:""{0}"", accepted-host-key:""{1}""", - hex.ReportedHostKey, - hex.AcceptedHostKey - )); - } - } - finally - { - if (wrapper != null) - wrapper.DisposeModules(); - } - } - - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - - if (parts.Length <= 1) - { - info.ReportClientError("No url or operation supplied", System.Net.HttpStatusCode.BadRequest); - return; - } - - var url = Library.Utility.Uri.UrlDecode(parts.First()); - var operation = parts.Last().ToLowerInvariant(); - - switch (operation) - { - case "dbpath": - LocateDbUri(url, info); - return; - case "list": - ListFolder(url, info); - return; - case "create": - CreateFolder(url, info); - return; - case "test": - TestConnection(url, info); - return; - default: - info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); - return; - } - } - - public void POST(string key, RequestInfo info) - { - string url; - - using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) - { - try - { - url = sr.ReadToEnd(); - } - catch (InvalidOperationException) - { - url = sr.ReadLineAsync().Result; - } - } - - Console.WriteLine(url); - - switch (key) - { - case "dbpath": - LocateDbUri(url, info); - return; - case "list": - ListFolder(url, info); - return; - case "create": - CreateFolder(url, info); - return; - case "put": - UploadFile(url, info); - return; - case "test": - TestConnection(url, info); - return; - default: - info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); - return; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs deleted file mode 100644 index f0c511ef4..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs +++ /dev/null @@ -1,117 +0,0 @@ -// 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; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class RequestInfo : IDisposable - { - public HttpServer.IHttpRequest Request { get; } - public HttpServer.IHttpResponse Response { get; } - public HttpServer.Sessions.IHttpSession Session { get; private set; } - public BodyWriter BodyWriter { get; private set; } - public RequestInfo(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - Request = request; - Response = response; - Session = session; - BodyWriter = new BodyWriter(response, request); - } - - public void ReportServerError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError) - { - Response.Status = code; - Response.Reason = message; - - BodyWriter.WriteJsonObject(new { Error = message }); - } - - public void ReportClientError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.BadRequest) - { - ReportServerError(message, code); - } - - public bool LongPollCheck(EventPollNotify poller, ref long id, out bool isError) - { - HttpServer.HttpInput input = String.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? Request.Form : Request.QueryString; - if (Library.Utility.Utility.ParseBool(input["longpoll"].Value, false)) - { - long lastEventId; - if (!long.TryParse(input["lasteventid"].Value, out lastEventId)) - { - ReportClientError("When activating long poll, the request must include the last event id", System.Net.HttpStatusCode.BadRequest); - isError = true; - return false; - } - - TimeSpan ts; - try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); } - catch (Exception ex) - { - ReportClientError("Invalid duration: " + ex.Message, System.Net.HttpStatusCode.BadRequest); - isError = true; - return false; - } - - if (ts <= TimeSpan.FromSeconds(10) || ts.TotalMilliseconds > int.MaxValue) - { - ReportClientError("Invalid duration, must be at least 10 seconds, and less than " + int.MaxValue + " milliseconds", System.Net.HttpStatusCode.BadRequest); - isError = true; - return false; - } - - isError = false; - id = poller.Wait(lastEventId, (int)ts.TotalMilliseconds); - return true; - } - - isError = false; - return false; - } - - public void OutputOK(object item = null) - { - BodyWriter.OutputOK(item); - } - - public void OutputError(object item = null, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError, string reason = null) - { - Response.Status = code; - Response.Reason = reason ?? "Error"; - if(item == null && reason != null) - { - item = new { Error = reason }; - } - BodyWriter.WriteJsonObject(item); - } - - public void Dispose() - { - if (BodyWriter != null) - { - var bw = BodyWriter; - BodyWriter = null; - bw.Dispose(); - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs deleted file mode 100644 index 418589837..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs +++ /dev/null @@ -1,116 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class ServerSetting : IRESTMethodGET, IRESTMethodPUT, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing"); - return; - } - - if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase)) - { - info.OutputOK(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate == null ? "False" : "True"); - return; - } - - if (key.StartsWith("--", StringComparison.Ordinal)) - { - var prop = FIXMEGlobal.DataConnection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); - info.OutputOK(prop == null ? null : prop.Value); - } - else - { - var prop = typeof(Database.ServerSettings).GetProperty(key); - if (prop == null) - info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found"); - else - info.OutputOK(prop.GetValue(FIXMEGlobal.DataConnection.ApplicationSettings)); - } - } - - public void PUT(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing"); - return; - } - - if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase)) - { - info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Can only update SSL certificate from commandline"); - return; - } - - if (key.StartsWith("--", StringComparison.Ordinal)) - { - var settings = FIXMEGlobal.DataConnection.Settings.ToList(); - - var prop = settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); - if (prop == null) - settings.Add(prop = new Database.Setting() { Name = key, Value = info.Request.Form["data"].Value }); - else - prop.Value = info.Request.Form["data"].Value; - - FIXMEGlobal.DataConnection.Settings = settings.ToArray(); - - info.OutputOK(prop == null ? null : prop.Value); - } - else - { - var prop = typeof(Database.ServerSettings).GetProperty(key); - if (prop == null) - info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found"); - else - { - var dict = new Dictionary(); - dict[key] = info.Request.Form["data"].Value; - FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(dict, false); - info.OutputOK(); - } - } - } - - public string Description { get { return "Return a list of settings for the server"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(string)), - new KeyValuePair(HttpServer.Method.Put, typeof(string)) - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs deleted file mode 100644 index 77258f307..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs +++ /dev/null @@ -1,136 +0,0 @@ -// 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; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Server.Database; -using System.IO; -using Duplicati.Server.Serialization; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class ServerSettings : IRESTMethodGET, IRESTMethodPATCH, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - // Join server settings and global settings - var adv_props = - FIXMEGlobal.DataConnection.GetSettings(Database.Connection.SERVER_SETTINGS_ID) - .Where(x => !string.IsNullOrWhiteSpace(x.Name)) - .Union( - FIXMEGlobal.DataConnection.Settings - .Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name.StartsWith("--", StringComparison.Ordinal)) - ); - - var dict = new Dictionary(); - foreach (var n in adv_props) - dict[n.Name] = n.Value; - - string sslcert; - dict.TryGetValue("server-ssl-certificate", out sslcert); - dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString(); - - info.OutputOK(dict); - } - - public void PATCH(string key, RequestInfo info) - { - string str = info.Request.Form["data"].Value; - - if (string.IsNullOrWhiteSpace(str)) - str = System.Threading.Tasks.Task.Run(async () => - { - using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) - - return await sr.ReadToEndAsync(); - }).GetAwaiter().GetResult(); - - if (string.IsNullOrWhiteSpace(str)) - { - info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest); - return; - } - - Dictionary data = null; - try - { - data = Serializer.Deserialize>(new StringReader(str)); - if (data == null) - { - info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest); - return; - } - - // Split into server settings and global settings - - var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value); - var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal)); - - serversettings.Remove("server-ssl-certificate"); - serversettings.Remove("ServerSSLCertificate"); - - if (serversettings.Any()) - FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false); - - if (globalsettings.Any()) - { - // Update based on inputs - var existing = FIXMEGlobal.DataConnection.Settings.ToDictionary(x => x.Name, x => x); - foreach (var g in globalsettings) - if (g.Value == null) - existing.Remove(g.Key); - else - { - if (existing.ContainsKey(g.Key)) - existing[g.Key].Value = g.Value; - else - existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value }; - } - - FIXMEGlobal.DataConnection.Settings = existing.Select(x => x.Value).ToArray(); - } - - info.OutputOK(); - } - catch (Exception ex) - { - if (data == null) - info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); - else - info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); - } - } - - public string Description { get { return "Return a list of settings for the server"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(Database.ServerSettings)) - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs deleted file mode 100644 index 70c382b1d..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs +++ /dev/null @@ -1,105 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class ServerState : IRESTMethodPOST, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - bool isError; - long id = 0; - long.TryParse(key, out id); - - var serverStatus = new Serializable.ServerStatus(); - if (info.LongPollCheck(FIXMEGlobal.StatusEventNotifyer, ref id, out isError)) - { - //Make sure we do not report a higher number than the eventnotifier says - var st = serverStatus; - st.LastEventID = id; - info.OutputOK(st); - } - else if (!isError) - { - info.OutputOK(serverStatus); - } - } - - public void POST(string key, RequestInfo info) - { - var input = info.Request.Form; - switch ((key ?? "").ToLowerInvariant()) - { - case "pause": - if (input.Contains("duration") && !string.IsNullOrWhiteSpace(input["duration"].Value)) - { - TimeSpan ts; - try - { - ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); - } - catch (Exception ex) - { - info.ReportClientError(ex.Message, System.Net.HttpStatusCode.BadRequest); - return; - } - if (ts.TotalMilliseconds > 0) - FIXMEGlobal.LiveControl.Pause(ts); - else - FIXMEGlobal.LiveControl.Pause(); - } - else - { - FIXMEGlobal.LiveControl.Pause(); - } - - info.OutputOK(); - return; - - case "resume": - FIXMEGlobal.LiveControl.Resume(); - info.OutputOK(); - return; - - default: - info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound); - return; - } - } - - public string Description { get { return "Return the state of the server. This method can be long-polled."; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(Serializable.ServerStatus)), - new KeyValuePair(HttpServer.Method.Post, typeof(Serializable.ServerStatus)) - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs deleted file mode 100644 index 93d5f780c..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs +++ /dev/null @@ -1,126 +0,0 @@ -// 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; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Library.Interface; -using Duplicati.Library.Common; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class SystemInfo : IRESTMethodGET, IRESTMethodDocumented - { - public string Description { get { return "Gets various system properties"; } } - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, SystemData(null).GetType()) - }; - } - } - - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.BodyWriter.OutputOK(SystemData(info)); - } - else if (key.Equals("filtergroups", StringComparison.OrdinalIgnoreCase)) - { - info.BodyWriter.OutputOK(FilterGroups()); - } - else - { - info.OutputError(code: System.Net.HttpStatusCode.NotFound, reason: "Not found"); - } - } - - private static object SystemData(RequestInfo info) - { - var browserlanguage = RESTHandler.ParseDefaultRequestCulture(info) ?? System.Globalization.CultureInfo.InvariantCulture; - - return new - { - APIVersion = 1, - PasswordPlaceholder = Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, - ServerVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), - ServerVersionName = Duplicati.License.VersionNumbers.Version, - ServerVersionType = Duplicati.Library.AutoUpdater.UpdaterManager.SelfVersion.ReleaseType, - StartedBy = FIXMEGlobal.Origin, - DefaultUpdateChannel = Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel, - DefaultUsageReportLevel = Duplicati.Library.UsageReporter.Reporter.DefaultReportLevel, - ServerTime = DateTime.Now, - OSType = Platform.IsClientPosix ? (Platform.IsClientOSX ? "OSX" : "Linux") : "Windows", - DirectorySeparator = System.IO.Path.DirectorySeparatorChar, - PathSeparator = System.IO.Path.PathSeparator, - CaseSensitiveFilesystem = Duplicati.Library.Utility.Utility.IsFSCaseSensitive, - MachineName = System.Environment.MachineName, - PackageTypeId = Duplicati.Library.AutoUpdater.UpdaterManager.PackageTypeId, - UserName = OperatingSystem.IsWindows() ? System.Security.Principal.WindowsIdentity.GetCurrent().Name : System.Environment.UserName, - NewLine = System.Environment.NewLine, - CLRVersion = System.Environment.Version.ToString(), - CLROSInfo = new - { - Platform = System.Environment.OSVersion.Platform.ToString(), - ServicePack = System.Environment.OSVersion.ServicePack, - Version = System.Environment.OSVersion.Version.ToString(), - VersionString = System.Environment.OSVersion.VersionString - }, - Options = Serializable.ServerSettings.Options, - CompressionModules = Serializable.ServerSettings.CompressionModules, - EncryptionModules = Serializable.ServerSettings.EncryptionModules, - BackendModules = Serializable.ServerSettings.BackendModules, - GenericModules = Serializable.ServerSettings.GenericModules, - WebModules = Serializable.ServerSettings.WebModules, - ConnectionModules = Serializable.ServerSettings.ConnectionModules, - ServerModules = Serializable.ServerSettings.ServerModules, - UsingAlternateUpdateURLs = Duplicati.Library.AutoUpdater.AutoUpdateSettings.UsesAlternateURLs, - LogLevels = Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)), - SpecialFolders = from n in SpecialFolders.Nodes select new { ID = n.id, Path = n.resolvedpath }, - BrowserLocale = new - { - Code = browserlanguage.Name, - EnglishName = browserlanguage.EnglishName, - DisplayName = browserlanguage.NativeName - }, - SupportedLocales = - Library.Localization.LocalizationService.SupportedCultures - .Select(x => new - { - Code = x, - EnglishName = new System.Globalization.CultureInfo(x).EnglishName, - DisplayName = new System.Globalization.CultureInfo(x).NativeName - } - ), - BrowserLocaleSupported = Library.Localization.LocalizationService.isCultureSupported(browserlanguage) - }; - } - - private static object FilterGroups() - { - return new { FilterGroups = Library.Utility.FilterGroups.GetFilterStringMap() }; - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs deleted file mode 100644 index 09eebe706..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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; -using System.Collections.Generic; -using Duplicati.Library.Interface; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class SystemWideSettings : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - info.OutputOK(new Duplicati.Library.Main.Options(new Dictionary()).SupportedCommands); - } - - public string Description { get { return "Return a list of settings that can be applied to all backups on a system-wide basis"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[])) - }; - } - } - - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Tags.cs b/Duplicati.Library.RestAPI/RESTMethods/Tags.cs deleted file mode 100644 index 90333250a..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Tags.cs +++ /dev/null @@ -1,59 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Tags : IRESTMethodGET, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - var r = - from n in - Serializable.ServerSettings.CompressionModules - .Union(Serializable.ServerSettings.EncryptionModules) - .Union(Serializable.ServerSettings.BackendModules) - .Union(Serializable.ServerSettings.GenericModules) - select n.Key.ToLower(CultureInfo.InvariantCulture); - - // Append all known tags - r = r.Union(from n in FIXMEGlobal.DataConnection.Backups select n.Tags into p from x in p select x.ToLower(CultureInfo.InvariantCulture)); - info.OutputOK(r); - } - - public string Description { get { return "Gets the list of tags"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(string[])), - }; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Task.cs b/Duplicati.Library.RestAPI/RESTMethods/Task.cs deleted file mode 100644 index 1af624e61..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Task.cs +++ /dev/null @@ -1,113 +0,0 @@ -// 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; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Task : IRESTMethodGET, IRESTMethodPOST - { - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - long taskid; - if (long.TryParse(parts.FirstOrDefault(), out taskid)) - { - var task = FIXMEGlobal.WorkThread.CurrentTask; - var tasks = FIXMEGlobal.WorkThread.CurrentTasks; - - if (task != null && task.TaskID == taskid) - { - info.OutputOK(new { Status = "Running" }); - return; - } - - if (tasks.FirstOrDefault(x => x.TaskID == taskid) == null) - { - KeyValuePair[] matches; - lock(FIXMEGlobal.MainLock) - matches = FIXMEGlobal.TaskResultCache.Where(x => x.Key == taskid).ToArray(); - - if (matches.Length == 0) - info.ReportClientError("No such task found", System.Net.HttpStatusCode.NotFound); - else - info.OutputOK(new { - Status = matches[0].Value == null ? "Completed" : "Failed", - ErrorMessage = matches[0].Value == null ? null : matches[0].Value.Message, - Exception = matches[0].Value == null ? null : matches[0].Value.ToString() - }); - } - else - { - info.OutputOK(new { Status = "Waiting" }); - } - } - else - { - info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest); - } - } - - public void POST(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - long taskid; - if (parts.Length == 2 && long.TryParse(parts.First(), out taskid)) - { - var task = FIXMEGlobal.WorkThread.CurrentTask; - var tasks = FIXMEGlobal.WorkThread.CurrentTasks; - - if (task != null) - tasks.Insert(0, task); - - task = tasks.FirstOrDefault(x => x.TaskID == taskid); - if (task == null) - { - info.ReportClientError("No such task", System.Net.HttpStatusCode.NotFound); - return; - } - - switch (parts.Last().ToLowerInvariant()) - { - case "stopaftercurrentfile": - task.Stop(allowCurrentFileToFinish: true); - info.OutputOK(); - return; - - case "stopnow": - task.Stop(allowCurrentFileToFinish: false); - info.OutputOK(); - return; - - case "abort": - task.Abort(); - info.OutputOK(); - return; - } - } - - info.ReportClientError("Invalid or missing task id", System.Net.HttpStatusCode.NotFound); - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs b/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs deleted file mode 100644 index cd900c522..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Tasks : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - var cur = FIXMEGlobal.WorkThread.CurrentTask; - var n = FIXMEGlobal.WorkThread.CurrentTasks; - - if (cur != null) - n.Insert(0, cur); - - info.OutputOK(n); - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs b/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs deleted file mode 100644 index 37330516d..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs +++ /dev/null @@ -1,82 +0,0 @@ -// 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; -using System.Collections.Generic; -using Duplicati.Server.Serialization; -using System.IO; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class UISettings : IRESTMethodGET, IRESTMethodPOST, IRESTMethodPATCH - { - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.OutputOK(FIXMEGlobal.DataConnection.GetUISettingsSchemes()); - } - else - { - info.OutputOK(FIXMEGlobal.DataConnection.GetUISettings(key)); - } - } - - public void POST(string key, RequestInfo info) - { - PATCH(key, info); - } - - public void PATCH(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.ReportClientError("Scheme is missing", System.Net.HttpStatusCode.BadRequest); - return; - } - - IDictionary data; - try - { - data = Serializer.Deserialize>(new StreamReader(info.Request.Body)); - } - catch (Exception ex) - { - info.ReportClientError(string.Format("Unable to parse settings object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); - return; - } - - if (data == null) - { - info.ReportClientError("Unable to parse settings object", System.Net.HttpStatusCode.BadRequest); - return; - } - - if (info.Request.Method == "POST") - FIXMEGlobal.DataConnection.SetUISettings(key, data); - else - FIXMEGlobal.DataConnection.UpdateUISettings(key, data); - info.OutputOK(); - } - - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/Updates.cs b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs deleted file mode 100644 index 94abc4491..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/Updates.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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 Duplicati.Library.RestAPI; -using System; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Updates : IRESTMethodPOST - { - public void POST(string key, RequestInfo info) - { - switch ((key ?? "").ToLowerInvariant()) - { - case "check": - FIXMEGlobal.UpdatePoller.CheckNow(); - info.OutputOK(); - return; - - default: - info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound); - return; - } - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs deleted file mode 100644 index 676b3c963..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class WebModule : IRESTMethodPOST - { - public void POST(string key, RequestInfo info) - { - var m = Duplicati.Library.DynamicLoader.WebLoader.Modules.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (m == null) - { - info.ReportClientError(string.Format("No such command {0}", key), System.Net.HttpStatusCode.NotFound); - return; - } - - info.OutputOK(new { - Status = "OK", - Result = m.Execute(info.Request.Form.Where(x => !x.Name.Equals("command", StringComparison.OrdinalIgnoreCase) - ).ToDictionary(x => x.Name, x => x.Value)) - }); - } - } -} - diff --git a/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs deleted file mode 100644 index 9f90d91db..000000000 --- a/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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; -using System.Linq; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class WebModules : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - info.OutputOK(Duplicati.Library.DynamicLoader.WebLoader.Modules); - } - } -} - diff --git a/Duplicati.Library.RestAPI/Runner.cs b/Duplicati.Library.RestAPI/Runner.cs index 5049c4100..95825d593 100644 --- a/Duplicati.Library.RestAPI/Runner.cs +++ b/Duplicati.Library.RestAPI/Runner.cs @@ -942,7 +942,7 @@ namespace Duplicati.Server return filter; } - internal static Dictionary GetCommonOptions() + public static Dictionary GetCommonOptions() { return (from n in FIXMEGlobal.DataConnection.Settings diff --git a/Duplicati.Library.RestAPI/Scheduler.cs b/Duplicati.Library.RestAPI/Scheduler.cs index 7544bb7c2..80d71d24f 100644 --- a/Duplicati.Library.RestAPI/Scheduler.cs +++ b/Duplicati.Library.RestAPI/Scheduler.cs @@ -1,5 +1,3 @@ -#region Disclaimer / License - // Copyright (C) 2015, The Duplicati Team // http://www.duplicati.com, info@duplicati.com // @@ -16,10 +14,8 @@ // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// -using Duplicati.Server.Serialization.Interface; -#endregion +using Duplicati.Server.Serialization.Interface; using System; using System.Collections.Generic; @@ -28,14 +24,16 @@ using System.Linq; using System.Threading; using Duplicati.Library.Utility; using Duplicati.Library.RestAPI; -using Duplicati.WebserverCore.Abstractions; + +// TODO: Rewrite this class. +// It should just signal what new backups to run, and not mix with the worker thread. namespace Duplicati.Server { /// /// This class handles scheduled runs of backups /// - public class Scheduler : IScheduler + public class Scheduler { private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); @@ -108,8 +106,8 @@ namespace Duplicati.Server public IList> GetSchedulerQueueIds() { return (from n in WorkerQueue - where n.Backup != null - select new Tuple(n.TaskID, n.Backup.ID)).ToList(); + where n.Backup != null + select new Tuple(n.TaskID, n.Backup.ID)).ToList(); } /// @@ -317,8 +315,8 @@ namespace Duplicati.Server { //See if it is already queued var tmplst = from n in m_worker.CurrentTasks - where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup - select n.Backup; + where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup + select n.Backup; var tastTemp = m_worker.CurrentTask; if (tastTemp != null && tastTemp.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup) @@ -393,9 +391,9 @@ namespace Duplicati.Server //Sort them, lock as we assign the m_schedule variable lock (m_lock) m_schedule = (from n in scheduled - where existing.ContainsKey(n.Key) - orderby n.Value.Value - select new KeyValuePair(n.Value.Value, existing[n.Key])).ToArray(); + where existing.ContainsKey(n.Key) + orderby n.Value.Value + select new KeyValuePair(n.Value.Value, existing[n.Key])).ToArray(); // Remove unused entries foreach (var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray()) diff --git a/Duplicati.Library.RestAPI/SpecialFolders.cs b/Duplicati.Library.RestAPI/SpecialFolders.cs index 5bcbd239f..78bc60a7e 100644 --- a/Duplicati.Library.RestAPI/SpecialFolders.cs +++ b/Duplicati.Library.RestAPI/SpecialFolders.cs @@ -31,7 +31,7 @@ namespace Duplicati.Server public static string ExpandEnvironmentVariables(string path) { - foreach(var n in Nodes) + foreach (var n in Nodes) if (path.StartsWith(n.id, StringComparison.Ordinal)) path = path.Replace(n.id, n.resolvedpath); return Environment.ExpandEnvironmentVariables(path); @@ -54,21 +54,21 @@ namespace Duplicati.Server }); } - public static string TranslateToPath(string str) + public static string TranslateToPath(string str) { string res; if (PathMap.TryGetValue(str, out res)) return res; - + return null; } - public static string TranslateToDisplayString(string str) + public static string TranslateToDisplayString(string str) { string res; if (DisplayMap.TryGetValue(str, out res)) return res; - + return null; } @@ -109,11 +109,11 @@ namespace Duplicati.Server { } } - + static SpecialFolders() { var lst = new List(); - + if (Platform.IsClientWindows) { TryAdd(lst, Environment.SpecialFolder.MyDocuments, "%MY_DOCUMENTS%", "My Documents"); @@ -147,7 +147,7 @@ namespace Duplicati.Server Nodes = lst.ToArray(); } - internal static Dictionary GetSourceNames(Serialization.Interface.IBackup backup) + public static Dictionary GetSourceNames(Serialization.Interface.IBackup backup) { if (backup.Sources == null || backup.Sources.Length == 0) return new Dictionary(); @@ -181,7 +181,7 @@ namespace Duplicati.Server // Handle duplicates var result = new Dictionary(); - foreach(var x in sources) + foreach (var x in sources) result[x.Key] = x.Value; return result; diff --git a/Duplicati.Library.RestAPI/Strings.cs b/Duplicati.Library.RestAPI/Strings.cs index d925113eb..aeb2ec12c 100644 --- a/Duplicati.Library.RestAPI/Strings.cs +++ b/Duplicati.Library.RestAPI/Strings.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using Duplicati.Library.Localization.Short; using System.Linq; -namespace Duplicati.Server.Strings { - public static class Program { +namespace Duplicati.Server.Strings +{ + public static class Program + { public static string AnotherInstanceDetected { get { return LC.L(@"Another instance is running, and was notified"); } } public static string DatabaseOpenError(string message) { return LC.L(@"Failed to create, open or upgrade the database. Error message: {0}", message); } @@ -42,11 +44,12 @@ Error message: {0}", error); } public static string ServerencryptionkeyLong(string envname, string decryptionoption) { return LC.L(@"This option sets the encryption key used to scramble the local settings database. This option can also be set with the environment variable {0}. Use the option --{1} to disable the database scrambling.", envname, decryptionoption); } public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } } public static string TempdirLong { get { return LC.L(@"This option can be used to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } } -} - internal static class Scheduler { + } + internal static class Scheduler + { public static string InvalidTimeSetupError(System.DateTime startdate, string interval, string alloweddays) { return LC.L(@"Unable to find a valid date, given the start date {0}, the repetition interval {1} and the allowed days {2}", startdate, interval, alloweddays); } } - internal static class Server + public static class Server { public static string DefectSSLCertInDatabase { get { return @"Unable to create SSL certificate using data from database. Starting without SSL."; } } public static string StartedServer(string ip, int port) { return LC.L(@"Server has started and is listening on {0}, port {1}", ip, port); } diff --git a/Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs b/Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs deleted file mode 100644 index a7ad72356..000000000 --- a/Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs +++ /dev/null @@ -1,342 +0,0 @@ -// 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; -using System.Collections.Concurrent; -using System.Linq; -using HttpServer; -using HttpServer.HttpModules; -using System.Collections.Generic; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer -{ - public class AuthenticationHandler : HttpModule - { - private const string AUTH_COOKIE_NAME = "session-auth"; - private const string NONCE_COOKIE_NAME = "session-nonce"; - - private const string XSRF_COOKIE_NAME = "xsrf-token"; - private const string XSRF_HEADER_NAME = "X-XSRF-Token"; - - private const string TRAYICONPASSWORDSOURCE_HEADER = "X-TrayIcon-PasswordSource"; - - public const string LOGIN_SCRIPT_URI = "/login.cgi"; - public const string LOGOUT_SCRIPT_URI = "/logout.cgi"; - public const string CAPTCHA_IMAGE_URI = RESTHandler.API_URI_PATH + "/captcha/"; - - private const int XSRF_TIMEOUT_MINUTES = 10; - private const int AUTH_TIMEOUT_MINUTES = 10; - - private readonly ConcurrentDictionary m_activeTokens = new ConcurrentDictionary(); - private readonly ConcurrentDictionary> m_activeNonces = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary m_activexsrf = new ConcurrentDictionary(); - - readonly System.Security.Cryptography.RandomNumberGenerator m_prng = System.Security.Cryptography.RNGCryptoServiceProvider.Create(); - - private string FindXSRFToken(HttpServer.IHttpRequest request) - { - string xsrftoken = request.Headers[XSRF_HEADER_NAME] ?? ""; - - if (string.IsNullOrWhiteSpace(xsrftoken)) - { - var xsrfq = request.Form[XSRF_HEADER_NAME] ?? request.Form[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)]; - xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value; - } - - if (string.IsNullOrWhiteSpace(xsrftoken)) - { - var xsrfq = request.QueryString[XSRF_HEADER_NAME] ?? request.QueryString[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)]; - xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value; - } - - return xsrftoken; - } - - private bool AddXSRFTokenToRespone(HttpServer.IHttpResponse response) - { - if (m_activexsrf.Count > 500) - return false; - - var buf = new byte[32]; - var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); - m_prng.GetBytes(buf); - var token = Convert.ToBase64String(buf); - - m_activexsrf.AddOrUpdate(token, key => expires, (key, existingExpires) => - { - // Simulate the original behavior => if the random token, against all odds, is already used - // we throw an ArgumentException - throw new ArgumentException("An element with the same key already exists in the dictionary."); - }); - - response.Cookies.Add(new HttpServer.ResponseCookie(XSRF_COOKIE_NAME, token, expires)); - return true; - } - - private string FindAuthCookie(HttpServer.IHttpRequest request) - { - var authcookie = request.Cookies[AUTH_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(AUTH_COOKIE_NAME)]; - var authform = request.Form["auth-token"] ?? request.Form[Library.Utility.Uri.UrlEncode("auth-token")]; - var authquery = request.QueryString["auth-token"] ?? request.QueryString[Library.Utility.Uri.UrlEncode("auth-token")]; - - var auth_token = string.IsNullOrWhiteSpace(authcookie?.Value) ? null : authcookie.Value; - if (!string.IsNullOrWhiteSpace(authquery?.Value)) - auth_token = authquery.Value; - if (!string.IsNullOrWhiteSpace(authform?.Value)) - auth_token = authform.Value; - - return auth_token; - } - - private bool HasXSRFCookie(HttpServer.IHttpRequest request) - { - // Clean up expired XSRF cookies - foreach (var k in (from n in m_activexsrf where DateTime.UtcNow > n.Value select n.Key)) - m_activexsrf.TryRemove(k, out _); - - var xsrfcookie = request.Cookies[XSRF_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(XSRF_COOKIE_NAME)]; - var value = xsrfcookie == null ? null : xsrfcookie.Value; - if (string.IsNullOrWhiteSpace(value)) - return false; - - if (m_activexsrf.ContainsKey(value)) - { - m_activexsrf[value] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); - return true; - } - else if (m_activexsrf.ContainsKey(Library.Utility.Uri.UrlDecode(value))) - { - m_activexsrf[Library.Utility.Uri.UrlDecode(value)] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); - return true; - } - - return false; - } - - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - HttpServer.HttpInput input = String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString; - - var auth_token = FindAuthCookie(request); - var xsrf_token = FindXSRFToken(request); - - if (!HasXSRFCookie(request)) - { - var cookieAdded = AddXSRFTokenToRespone(response); - - if (!cookieAdded) - { - response.Status = System.Net.HttpStatusCode.ServiceUnavailable; - response.Reason = "Too Many Concurrent Request, try again later"; - return true; - } - } - - if (LOGOUT_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) - { - if (!string.IsNullOrWhiteSpace(auth_token)) - { - // Remove the active auth token - m_activeTokens.TryRemove(auth_token, out _); - } - - response.Status = System.Net.HttpStatusCode.NoContent; - response.Reason = "OK"; - - return true; - } - else if (LOGIN_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) - { - // Remove expired nonces - foreach(var k in (from n in m_activeNonces where DateTime.UtcNow > n.Value.Item1 select n.Key)) - m_activeNonces.TryRemove(k, out _); - - if (input["get-nonce"] != null && !string.IsNullOrWhiteSpace(input["get-nonce"].Value)) - { - if (m_activeNonces.Count > 50) - { - response.Status = System.Net.HttpStatusCode.ServiceUnavailable; - response.Reason = "Too many active login attempts"; - return true; - } - - var password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword; - - if (request.Headers[TRAYICONPASSWORDSOURCE_HEADER] == "database") - password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordTrayIconHash; - - var buf = new byte[32]; - var expires = DateTime.UtcNow.AddMinutes(AUTH_TIMEOUT_MINUTES); - m_prng.GetBytes(buf); - var nonce = Convert.ToBase64String(buf); - - var sha256 = System.Security.Cryptography.SHA256.Create(); - sha256.TransformBlock(buf, 0, buf.Length, buf, 0); - buf = Convert.FromBase64String(password); - sha256.TransformFinalBlock(buf, 0, buf.Length); - var pwd = Convert.ToBase64String(sha256.Hash); - - m_activeNonces.AddOrUpdate(nonce, key => new Tuple(expires, pwd), (key, existingValue) => - { - // Simulate the original behavior => if the nonce, against all odds, is already used - // we throw an ArgumentException - throw new ArgumentException("An element with the same key already exists in the dictionary."); - }); - - response.Cookies.Add(new HttpServer.ResponseCookie(NONCE_COOKIE_NAME, nonce, expires)); - using(var bw = new BodyWriter(response, request)) - { - bw.OutputOK(new { - Status = "OK", - Nonce = nonce, - Salt = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordSalt - }); - } - return true; - } - else - { - if (input["password"] != null && !string.IsNullOrWhiteSpace(input["password"].Value)) - { - var nonce_el = request.Cookies[NONCE_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(NONCE_COOKIE_NAME)]; - var nonce = nonce_el == null || string.IsNullOrWhiteSpace(nonce_el.Value) ? "" : nonce_el.Value; - var urldecoded = nonce == null ? "" : Duplicati.Library.Utility.Uri.UrlDecode(nonce); - if (m_activeNonces.ContainsKey(urldecoded)) - nonce = urldecoded; - - if (!m_activeNonces.ContainsKey(nonce)) - { - response.Status = System.Net.HttpStatusCode.Unauthorized; - response.Reason = "Unauthorized"; - response.ContentType = "application/json"; - return true; - } - - var pwd = m_activeNonces[nonce].Item2; - - // Remove the nonce - m_activeNonces.TryRemove(nonce, out _); - - if (pwd != input["password"].Value) - { - response.Status = System.Net.HttpStatusCode.Unauthorized; - response.Reason = "Unauthorized"; - response.ContentType = "application/json"; - return true; - } - - var buf = new byte[32]; - var expires = DateTime.UtcNow.AddHours(1); - m_prng.GetBytes(buf); - var token = Duplicati.Library.Utility.Utility.Base64UrlEncode(buf); - while (token.Length > 0 && token.EndsWith("=", StringComparison.Ordinal)) - token = token.Substring(0, token.Length - 1); - - m_activeTokens.AddOrUpdate(token, key => expires, (key, existingValue) => - { - // Simulate the original behavior => if the token, against all odds, is already used - // we throw an ArgumentException - throw new ArgumentException("An element with the same key already exists in the dictionary."); - }); - - response.Cookies.Add(new HttpServer.ResponseCookie(AUTH_COOKIE_NAME, token, expires)); - - using(var bw = new BodyWriter(response, request)) - bw.OutputOK(); - - return true; - } - } - } - - var limitedAccess = - request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase) - ; - - // Override to allow the CAPTCHA call to go through - if (request.Uri.AbsolutePath.StartsWith(CAPTCHA_IMAGE_URI, StringComparison.OrdinalIgnoreCase) && request.Method == "GET") - limitedAccess = false; - - if (limitedAccess) - { - if (xsrf_token != null && m_activexsrf.ContainsKey(xsrf_token)) - { - var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES); - m_activexsrf[xsrf_token] = expires; - response.Cookies.Add(new ResponseCookie(XSRF_COOKIE_NAME, xsrf_token, expires)); - } - else - { - response.Status = System.Net.HttpStatusCode.BadRequest; - response.Reason = "Missing XSRF Token. Please reload the page"; - - return true; - } - } - - if (string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword)) - return false; - - foreach(var k in (from n in m_activeTokens where DateTime.UtcNow > n.Value select n.Key)) - m_activeTokens.TryRemove(k, out _); - - - // If we have a valid token, proceed - if (!string.IsNullOrWhiteSpace(auth_token)) - { - DateTime expires; - var found = m_activeTokens.TryGetValue(auth_token, out expires); - if (!found) - { - auth_token = Duplicati.Library.Utility.Uri.UrlDecode(auth_token); - found = m_activeTokens.TryGetValue(auth_token, out expires); - } - - if (found && DateTime.UtcNow < expires) - { - expires = DateTime.UtcNow.AddHours(1); - - m_activeTokens[auth_token] = expires; - response.Cookies.Add(new ResponseCookie(AUTH_COOKIE_NAME, auth_token, expires)); - return false; - } - } - - if ("/".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase) || "/index.html".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase)) - { - response.Redirect("/login.html"); - return true; - } - - if (limitedAccess) - { - response.Status = System.Net.HttpStatusCode.Unauthorized; - response.Reason = "Not logged in"; - response.AddHeader("Location", "login.html"); - - return true; - } - - return false; - } - } -} - diff --git a/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs b/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs deleted file mode 100644 index a411d98ac..000000000 --- a/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs +++ /dev/null @@ -1,124 +0,0 @@ -// 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; -using System.Globalization; -using System.Threading.Tasks; -using Duplicati.Server.Serialization; - -namespace Duplicati.Server.WebServer -{ - public class BodyWriter : IDisposable, IAsyncDisposable - { - private readonly HttpServer.IHttpResponse m_resp; - private readonly string m_jsonp; - private static readonly object SUCCESS_RESPONSE = new { Status = "OK" }; - private readonly System.IO.StreamWriter m_bodyStreamWriter; - - public BodyWriter(HttpServer.IHttpResponse resp, HttpServer.IHttpRequest request) - : this(resp, request.QueryString["jsonp"].Value) - { - } - - public BodyWriter(HttpServer.IHttpResponse resp, string jsonp) - { - m_bodyStreamWriter = new System.IO.StreamWriter(resp.Body, resp.Encoding); - m_resp = resp; - m_jsonp = jsonp; - if (!m_resp.HeadersSent) - m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); - } - public void SetOK() - { - m_resp.Reason = "OK"; - m_resp.Status = System.Net.HttpStatusCode.OK; - } - - public void OutputOK(object result = null) - { - SetOK(); - WriteJsonObject(result ?? SUCCESS_RESPONSE); - } - - public void WriteJsonObject(object o) - { - if (!m_resp.HeadersSent) - m_resp.ContentType = "application/json"; - - Task.Run(async () => { - - if (!string.IsNullOrEmpty(m_jsonp)) - { - await m_bodyStreamWriter.WriteAsync(m_jsonp); - await m_bodyStreamWriter.WriteAsync('('); - } - - var oldCulture = CultureInfo.CurrentCulture; - CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; - await Serializer.SerializeJsonAsync(m_bodyStreamWriter, o, true); - CultureInfo.CurrentCulture = oldCulture; - - if (!string.IsNullOrEmpty(m_jsonp)) - { - await m_bodyStreamWriter.WriteAsync(')'); - await m_bodyStreamWriter.FlushAsync(); - } - }).GetAwaiter().GetResult(); - } - - public void Dispose() - { - Task.Run(async () => { - await DisposeAsync(); - }).GetAwaiter().GetResult(); - } - - private bool disposed = false; - public async ValueTask DisposeAsync() - { - if(disposed) return; - disposed = true; - - if (!m_resp.HeadersSent) - { - await m_bodyStreamWriter.FlushAsync(); - m_resp.ContentLength = m_bodyStreamWriter.BaseStream.Length; - m_resp.Send(); - } - await m_bodyStreamWriter.DisposeAsync(); - } - - internal void Flush() - { - Task.Run(async () => { - await m_bodyStreamWriter.FlushAsync(); - }).GetAwaiter().GetResult(); - } - - internal void Write(string v) - { - Task.Run(async () => { - await m_bodyStreamWriter.WriteAsync(v); - }).GetAwaiter().GetResult(); - } - } - -} - diff --git a/Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs b/Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs deleted file mode 100644 index 4146e2768..000000000 --- a/Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs +++ /dev/null @@ -1,133 +0,0 @@ -// 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; -using System.Linq; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Text; - -namespace Duplicati.Server.WebServer -{ - /// - /// Helper class for creating Captcha images - /// - public static class CaptchaUtil - { - /// - /// A lookup string with characters to use - /// - private static readonly string DEFAULT_CHARS = "ACDEFGHJKLMNPQRTUVWXY34679"; - - /// - /// A range of possible brush colors - /// - private static readonly Brush[] BRUSH_COLORS = - typeof(Brushes) - .GetProperties(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public) - .Where(x => x.PropertyType == typeof(Brush)) - .Select(x => x.GetValue(null, null) as Brush) - .Where(x => x != null) - .ToArray(); - - /// - /// Approximate the size in pixels of text drawn at the given fontsize - /// - private static int ApproxTextWidth(string text, FontFamily fontfamily, int fontsize) - { - using (var font = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) - using (var graphics = Graphics.FromImage(new Bitmap(1, 1))) { - return (int) graphics.MeasureString(text, font).Width; - } - } - - /// - /// Creates a random answer. - /// - /// The random answer. - /// The list of allowed chars, supply a character multiple times to change frequency. - /// The minimum answer length. - /// The maximum answer length. - public static string CreateRandomAnswer(string allowedchars = null, int minlength = 10, int maxlength = 12) - { - allowedchars = allowedchars ?? DEFAULT_CHARS; - var rnd = new Random(); - var len = rnd.Next(Math.Min(minlength, maxlength), Math.Max(minlength, maxlength) + 1); - if (len <= 0) - throw new ArgumentException($"The values ${minlength} and ${maxlength} gave a final length of {len} and it must be greater than 0"); - - return new string(Enumerable.Range(0, len).Select(x => allowedchars[rnd.Next(0, allowedchars.Length)]).ToArray()); - } - - /// - /// Creates a captcha image. - /// - /// The captcha image. - /// The captcha solution string. - /// The size of the image, omit to get a size based on the string. - /// The size of the font used to create the captcha, in pixels. - public static Bitmap CreateCaptcha(string answer, Size size = default(Size), int fontsize = 40) - { - var fontfamily = FontFamily.GenericSansSerif; - var text_width = ApproxTextWidth(answer, fontfamily, fontsize); - if (size.Width == 0 || size.Height == 0) - size = new Size((int) (text_width * 1.2), (int) (fontsize * 1.2)); - - var bmp = new Bitmap(size.Width, size.Height); - var rnd = new Random(); - var stray_x = fontsize / 2; - var stray_y = size.Height / 4; - var ans_stray_x = fontsize / 3; - var ans_stray_y = size.Height / 6; - using (var graphics = Graphics.FromImage(bmp)) - using (var font1 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) - using (var font2 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel)) - using (var font3 = new HatchBrush(HatchStyle.Shingle, Color.GhostWhite, Color.DarkBlue)) - { - graphics.Clear(Color.White); - graphics.TextRenderingHint = TextRenderingHint.AntiAlias; - - // Apply a some background string to make it hard to do OCR - foreach (var color in new[] { Color.Yellow, Color.LightGreen, Color.GreenYellow }) - using (var brush = new SolidBrush(color)) - graphics.DrawString(CreateRandomAnswer(minlength: answer.Length, maxlength: answer.Length), font2, brush, rnd.Next(-stray_x, stray_x), rnd.Next(-stray_y, stray_y)); - - - var spacing = (size.Width / fontsize) + rnd.Next(0, stray_x); - - // Create a vertical background lines - for (var i = rnd.Next(0, stray_x); i < size.Width; i += spacing) - using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)])) - graphics.DrawLine(pen, i + rnd.Next(-stray_x, stray_x), rnd.Next(0, stray_y), i + rnd.Next(-stray_x, stray_x), size.Height - rnd.Next(0, stray_y)); - - spacing = (size.Height / fontsize) + rnd.Next(0, stray_y); - // Create a horizontal background lines - for (var i = rnd.Next(0, stray_y); i < size.Height; i += spacing) - using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)])) - graphics.DrawLine(pen, rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y), size.Width - rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y)); - - // Draw the actual answer - graphics.DrawString(answer, font1, font3, ((size.Width - text_width) / 2) + rnd.Next(-ans_stray_x, ans_stray_x), ((size.Height - fontsize) / 2) + rnd.Next(-ans_stray_y, ans_stray_y)); - - return bmp; - } - } - } -} diff --git a/Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs b/Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs deleted file mode 100644 index fe6c70478..000000000 --- a/Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs +++ /dev/null @@ -1,82 +0,0 @@ -// 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; -using System.Linq; -using HttpServer; -using HttpServer.HttpModules; -using HttpServer.Exceptions; -using Duplicati.Library.Common.IO; - -namespace Duplicati.Server.WebServer -{ - internal class IndexHtmlHandler : HttpModule - { - private readonly string m_webroot; - - private static readonly string[] ForbiddenChars = new string[] {"\\", "..", ":"}.Union(from n in System.IO.Path.GetInvalidPathChars() select n.ToString()).Distinct().ToArray(); - private static readonly string DirSep = Util.DirectorySeparatorString; - - public IndexHtmlHandler(string webroot) { m_webroot = webroot; } - - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - var path = this.GetPath(request.Uri); - var html = System.IO.Path.Combine(path, "index.html"); - var htm = System.IO.Path.Combine(path, "index.htm"); - - if (System.IO.Directory.Exists(path) && (System.IO.File.Exists(html) || System.IO.File.Exists(htm))) - { - if (!request.Uri.AbsolutePath.EndsWith("/", StringComparison.Ordinal)) - { - response.Redirect(request.Uri.AbsolutePath + "/"); - return true; - } - - response.Status = System.Net.HttpStatusCode.OK; - response.Reason = "OK"; - response.ContentType = "text/html; charset=utf-8"; - response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); - - using (var fs = System.IO.File.OpenRead(System.IO.File.Exists(html) ? html : htm)) - { - response.ContentLength = fs.Length; - response.Body = fs; - response.Send(); - } - - return true; - } - - return false; - } - - private string GetPath(Uri uri) - { - if (ForbiddenChars.Any(x => uri.AbsolutePath.Contains(x))) - throw new BadRequestException("Illegal path"); - var uripath = Uri.UnescapeDataString(uri.AbsolutePath); - while(uripath.Length > 0 && (uripath.StartsWith("/", StringComparison.Ordinal) || uripath.StartsWith(DirSep, StringComparison.Ordinal))) - uripath = uripath.Substring(1); - return System.IO.Path.Combine(m_webroot, uripath.Replace('/', System.IO.Path.DirectorySeparatorChar)); - } - } -} - diff --git a/Duplicati.Library.RestAPI/WebServer/RESTHandler.cs b/Duplicati.Library.RestAPI/WebServer/RESTHandler.cs deleted file mode 100644 index 10c147b76..000000000 --- a/Duplicati.Library.RestAPI/WebServer/RESTHandler.cs +++ /dev/null @@ -1,263 +0,0 @@ -// 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; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using HttpServer.HttpModules; - -using Duplicati.Server.WebServer.RESTMethods; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer -{ - public class RESTHandler : HttpModule - { - public const string API_URI_PATH = "/api/v1"; - public static readonly int API_URI_SEGMENTS = API_URI_PATH.Split(new char[] {'/'}).Length; - - private static readonly Dictionary _modules = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public static IDictionary Modules { get { return _modules; } } - - /// - /// Loads all REST modules in the Duplicati.Server.WebServer.RESTMethods namespace - /// - static RESTHandler() - { - var lst = - from n in typeof(IRESTMethod).Assembly.GetTypes() - where - n.Namespace == typeof(IRESTMethod).Namespace - && - typeof(IRESTMethod).IsAssignableFrom(n) - && - !n.IsAbstract - && - !n.IsInterface - select n; - - foreach(var t in lst) - { - var m = (IRESTMethod)Activator.CreateInstance(t); - _modules.Add(t.Name.ToLowerInvariant(), m); - } - } - - public static void HandleControlCGI(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, Type module) - { - var method = request.Method; - if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"])) - method = request.Headers["X-HTTP-Method-Override"]; - - DoProcess(request, response, session, method, module.Name.ToLowerInvariant(), (String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString)["id"].Value); - } - - private static readonly ConcurrentDictionary _cultureCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - private static System.Globalization.CultureInfo ParseRequestCulture(RequestInfo info) - { - // Inject the override - return ParseRequestCulture(string.Format("{0},{1}", info.Request.Headers["X-UI-Language"], info.Request.Headers["Accept-Language"])); - } - - public static System.Globalization.CultureInfo ParseDefaultRequestCulture(RequestInfo info) - { - if (info == null) - return null; - return ParseRequestCulture(info.Request.Headers["Accept-Language"]); - } - - private static System.Globalization.CultureInfo ParseRequestCulture(string acceptheader) - { - acceptheader = acceptheader ?? string.Empty; - - // Lock-free read - System.Globalization.CultureInfo ci; - if (_cultureCache.TryGetValue(acceptheader, out ci)) - return ci; - - // Lock-free assignment, we might compute the value twice - return _cultureCache[acceptheader] = - // Parse headers like "Accept-Language: da, en-gb;q=0.8, en;q=0.7" - acceptheader - .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => - { - var opts = x.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries); - var lang = opts.FirstOrDefault(); - var weight = - opts.Where(y => y.StartsWith("q=", StringComparison.OrdinalIgnoreCase)) - .Select(y => - { - float f; - float.TryParse(y.Substring(2), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f); - return f; - }).FirstOrDefault(); - - // Set the default weight=1 - if (weight <= 0.001 && weight >= 0) - weight = 1; - - return new KeyValuePair(lang, weight); - }) - // Handle priority - .OrderByDescending(x => x.Value) - .Select(x => x.Key) - .Distinct() - // Filter invalid/unsupported items - .Where(x => !string.IsNullOrWhiteSpace(x) && Library.Localization.LocalizationService.ParseCulture(x) != null) - .Select(x => Library.Localization.LocalizationService.ParseCulture(x)) - // And get the first that works - .FirstOrDefault(); - - } - - public static void DoProcess(RequestInfo info, string method, string module, string key) - { - var ci = ParseRequestCulture(info); - - using (Library.Localization.LocalizationService.TemporaryContext(ci)) - { - try - { - if (ci != null) - info.Response.AddHeader("Content-Language", ci.Name); - - IRESTMethod mod; - _modules.TryGetValue(module, out mod); - - if (mod == null) - { - info.Response.Status = System.Net.HttpStatusCode.NotFound; - info.Response.Reason = "No such module"; - } - else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get) - { - if (info.Request.Form != HttpServer.HttpForm.EmptyForm) - { - if (info.Request.QueryString == HttpServer.HttpInput.Empty) - { - var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - r.SetValue(info.Request, new HttpServer.HttpInput("formdata")); - } - - foreach (HttpServer.HttpInputItem v in info.Request.Form) - if (!info.Request.QueryString.Contains(v.Name)) - info.Request.QueryString.Add(v.Name, v.Value); - } - - get.GET(key, info); - } - else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put) - put.PUT(key, info); - else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post) - { - if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty) - { - var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] {typeof(HttpServer.HttpForm)}, null); - r.Invoke(info.Request, new object[] {new HttpServer.HttpForm(info.Request.QueryString)}); - } - else - { - foreach (HttpServer.HttpInputItem v in info.Request.QueryString) - if (!info.Request.Form.Contains(v.Name)) - info.Request.Form.Add(v.Name, v.Value); - } - - post.POST(key, info); - } - else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete) - delete.DELETE(key, info); - else if (method == "PATCH" && mod is IRESTMethodPATCH patch) - patch.PATCH(key, info); - else - { - info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed; - info.Response.Reason = "Method is not allowed"; - } - } - catch (Exception ex) - { - FIXMEGlobal.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex); - Console.WriteLine(ex); - - try - { - if (!info.Response.HeadersSent) - { - info.Response.Status = System.Net.HttpStatusCode.InternalServerError; - info.Response.Reason = "Error"; - info.Response.ContentType = "text/plain"; - - var wex = ex; - while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex) - wex = wex.InnerException; - - info.BodyWriter.WriteJsonObject(new - { - Message = wex.Message, - Type = wex.GetType().Name, -#if DEBUG - Stacktrace = wex.ToString() -#endif - }); - info.BodyWriter.Flush(); - } - } - catch (Exception flex) - { - FIXMEGlobal.DataConnection.LogError("", "Reporting error gave error", flex); - } - } - } - } - - public static void DoProcess(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, string method, string module, string key) - { - using(var reqinfo = new RequestInfo(request, response, session)) - DoProcess(reqinfo, method, module, key); - } - - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - if (!request.Uri.AbsolutePath.StartsWith(API_URI_PATH, StringComparison.OrdinalIgnoreCase)) - return false; - - var module = request.Uri.Segments.Skip(API_URI_SEGMENTS).FirstOrDefault(); - if (string.IsNullOrWhiteSpace(module)) - module = "help"; - - module = module.Trim('/'); - - var key = string.Join("", request.Uri.Segments.Skip(API_URI_SEGMENTS + 1)).Trim('/'); - - var method = request.Method; - if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"])) - method = request.Headers["X-HTTP-Method-Override"]; - - DoProcess(request, response, session, method, module, key); - - return true; - } - } -} - diff --git a/Duplicati.Library.RestAPI/WebServer/Server.cs b/Duplicati.Library.RestAPI/WebServer/Server.cs deleted file mode 100644 index c215c5ab5..000000000 --- a/Duplicati.Library.RestAPI/WebServer/Server.cs +++ /dev/null @@ -1,441 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using HttpServer.HttpModules; -using System.Security.Cryptography.X509Certificates; -using Duplicati.Library.Common.IO; -using Duplicati.Library.RestAPI; - -namespace Duplicati.Server.WebServer -{ - public class Server - { - /// - /// The tag used for logging - /// - private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); - - /// - /// Option for changing the webroot folder - /// - public const string OPTION_WEBROOT = "webservice-webroot"; - - /// - /// Option for changing the webservice listen port - /// - public const string OPTION_PORT = "webservice-port"; - - /// - /// Option for changing the webservice listen interface - /// - public const string OPTION_INTERFACE = "webservice-interface"; - - /// - /// The default path to the web root - /// - public const string DEFAULT_OPTION_WEBROOT = "webroot"; - - /// - /// The default listening port - /// - public const int DEFAULT_OPTION_PORT = 8200; - - /// - /// Option for setting the webservice SSL certificate - /// - public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile"; - - /// - /// Option for setting the webservice SSL certificate key - /// - public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword"; - - /// - /// The default listening interface - /// - public const string DEFAULT_OPTION_INTERFACE = "loopback"; - - /// - /// The single webserver instance - /// - private readonly HttpServer.HttpServer m_server; - - /// - /// The webserver listening port - /// - public readonly int Port; - - /// - /// A string that is sent out instead of password values - /// - public const string PASSWORD_PLACEHOLDER = "**********"; - - /// - /// Sets up the webserver and starts it - /// - /// A set of options - public Server(IDictionary options) - { - string portstring; - IEnumerable ports = null; - options.TryGetValue(OPTION_PORT, out portstring); - if (!string.IsNullOrEmpty(portstring)) - ports = - from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - where int.TryParse(n, out _) - select int.Parse(n); - - if (ports == null || !ports.Any()) - ports = new int[] { DEFAULT_OPTION_PORT }; - - string interfacestring; - System.Net.IPAddress listenInterface; - options.TryGetValue(OPTION_INTERFACE, out interfacestring); - - if (string.IsNullOrWhiteSpace(interfacestring)) - interfacestring = FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface; - if (string.IsNullOrWhiteSpace(interfacestring)) - interfacestring = DEFAULT_OPTION_INTERFACE; - - if (interfacestring.Trim() == "*" || interfacestring.Trim().Equals("any", StringComparison.OrdinalIgnoreCase) || interfacestring.Trim().Equals("all", StringComparison.OrdinalIgnoreCase)) - listenInterface = System.Net.IPAddress.Any; - else if (interfacestring.Trim() == "loopback") - listenInterface = System.Net.IPAddress.Loopback; - else - listenInterface = System.Net.IPAddress.Parse(interfacestring); - - string certificateFile; - options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out certificateFile); - - string certificateFilePassword; - options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out certificateFilePassword); - - X509Certificate2 cert = null; - bool certValid = false; - - if (certificateFile == null) - { - try - { - cert = FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate; - - if (cert != null) - certValid = cert.HasPrivateKey; - } - catch (Exception ex) - { - Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase); - } - } - else if (certificateFile.Length == 0) - { - FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = null; - } - else - { - try - { - if (string.IsNullOrWhiteSpace(certificateFilePassword)) - cert = new X509Certificate2(certificateFile, "", X509KeyStorageFlags.Exportable); - else - cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable); - - certValid = cert.HasPrivateKey; - } - catch (Exception ex) - { - throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex); - } - } - - // If we are in hosted mode with no specified port, - // then try different ports - foreach (var p in ports) - try - { - // Due to the way the server is initialized, - // we cannot try to start it again on another port, - // so we create a new server for each attempt - - var server = CreateServer(options); - - if (certValid) - { - server.Start(listenInterface, p, cert, System.Security.Authentication.SslProtocols.None, null, false); - } - else - { - server.Start(listenInterface, p); - } - - m_server = server; - m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); - this.Port = p; - - if (interfacestring != FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface) - FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring; - - if (certValid && !cert.Equals(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate)) - FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = cert; - - Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p)); - - return; - } - catch (System.Net.Sockets.SocketException) - { - } - - throw new Exception(Strings.Server.ServerStartFailure(ports)); - } - - private static void AddMimeTypes(FileModule fm) - { - fm.AddDefaultMimeTypes(); - fm.MimeTypes["htc"] = "text/x-component"; - fm.MimeTypes["json"] = "application/json"; - fm.MimeTypes["map"] = "application/json"; - fm.MimeTypes["htm"] = "text/html; charset=utf-8"; - fm.MimeTypes["html"] = "text/html; charset=utf-8"; - fm.MimeTypes["hbs"] = "application/x-handlebars-template"; - fm.MimeTypes["woff"] = "application/font-woff"; - fm.MimeTypes["woff2"] = "application/font-woff"; - } - - private static HttpServer.HttpServer CreateServer(IDictionary options) - { - HttpServer.HttpServer server = new HttpServer.HttpServer(); - - server.Add(new HostHeaderChecker()); - - if (string.Equals(Environment.GetEnvironmentVariable("SYNO_DSM_AUTH") ?? string.Empty, "1")) - server.Add(new SynologyAuthenticationHandler()); - - server.Add(new AuthenticationHandler()); - - server.Add(new RESTHandler()); - - string webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR; - string install_webroot = System.IO.Path.Combine(webroot, "webroot"); - -#if DEBUG - // Easy test for extensions while debugging - install_webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR; - - if (!System.IO.Directory.Exists(System.IO.Path.Combine(webroot, "webroot"))) - { - //For debug we go "../../../.." to get out of "GUI/Duplicati.GUI.TrayIcon/bin/debug" - string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", "..")); - tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Server"); - if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) - webroot = tmpwebroot; - else - { - //If we are running the server standalone, we only need to exit "bin/Debug" - tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..")); - if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) - webroot = tmpwebroot; - } - } -#endif - - webroot = System.IO.Path.Combine(webroot, "webroot"); - - if (options.ContainsKey(OPTION_WEBROOT)) - { - string userroot = options[OPTION_WEBROOT]; -#if DEBUG - //In debug mode we do not care where the path points -#else - //In release mode we check that the user supplied path is located - // in the same folders as the running application, to avoid users - // that inadvertently expose top level folders - if (!string.IsNullOrWhiteSpace(userroot) - && userroot.StartsWith(Util.AppendDirSeparator(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison)) -#endif - { - webroot = userroot; - install_webroot = webroot; - } - } - - if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "customized"))) - { - var customized_files = new CacheControlFileHandler("/customized/", System.IO.Path.Combine(install_webroot, "customized")); - AddMimeTypes(customized_files); - server.Add(customized_files); - } - - if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "oem"))) - { - var oem_files = new CacheControlFileHandler("/oem/", System.IO.Path.Combine(install_webroot, "oem")); - AddMimeTypes(oem_files); - server.Add(oem_files); - } - - if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "package"))) - { - var proxy_files = new CacheControlFileHandler("/proxy/", System.IO.Path.Combine(install_webroot, "package")); - AddMimeTypes(proxy_files); - server.Add(proxy_files); - } - - var fh = new CacheControlFileHandler("/", webroot, true); - AddMimeTypes(fh); - server.Add(fh); - - server.Add(new IndexHtmlHandler(webroot)); -#if DEBUG - //For debugging, it is nice to know when we get a 404 - server.Add(new DebugReportHandler()); -#endif - return server; - } - - private class DebugReportHandler : HttpModule - { - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - System.Diagnostics.Trace.WriteLine(string.Format("Rejecting request for {0}", request.Uri)); - return false; - } - } - - private class CacheControlFileHandler : FileModule - { - public CacheControlFileHandler(string baseUri, string basePath, bool useLastModifiedHeader = false) - : base(baseUri, basePath, useLastModifiedHeader) - { - - } - - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - if (!this.CanHandle(request.Uri)) - return false; - - if (request.Uri.AbsolutePath.EndsWith("index.html", StringComparison.Ordinal) || request.Uri.AbsolutePath.EndsWith("index.htm", StringComparison.Ordinal)) - response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0"); - else - response.AddHeader("Cache-Control", "max-age=" + (60 * 60 * 24)); - return base.Process(request, response, session); - } - } - - /// - /// Module for injecting host header verification - /// - private class HostHeaderChecker : HttpModule - { - /// - /// The hostnames that we allow - /// - private string[] m_lastSplitNames; - - /// - /// The string used to generate m_lastSplitNames; - /// - private string m_lastAllowed; - - /// - /// A regex to detect potential IPv4 addresses. - /// Note that this also detects things that are not valid IPv4. - /// - private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}"); - /// - /// A regex to detect potential IPv6 addresses. - /// Note that this also detects things that are not valid IPv6. - /// - private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?"); - - /// - /// The hostnames that are always allowed - /// - private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" }; - - /// - /// Process the received request - /// - /// A flag indicating if the request is handled. - /// The received request. - /// The response object. - /// The session state. - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - string[] h = null; - var hstring = FIXMEGlobal.DataConnection.ApplicationSettings.AllowedHostnames; - - if (!string.IsNullOrWhiteSpace(hstring)) - { - h = m_lastSplitNames; - if (hstring != m_lastAllowed) - { - m_lastAllowed = hstring; - h = m_lastSplitNames = (hstring ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - } - - if (h == null || h.Length == 0) - h = null; - } - - // For some reason, the web server strips out the host header - var host = request.Headers["Host"]; - if (string.IsNullOrWhiteSpace(host)) - host = request.Uri.Host; - - // This should not happen - if (string.IsNullOrWhiteSpace(host)) - { - response.Reason = "Invalid request, missing host header"; - response.Status = System.Net.HttpStatusCode.Forbidden; - var msg = System.Text.Encoding.ASCII.GetBytes(response.Reason); - response.ContentType = "text/plain"; - response.ContentLength = msg.Length; - response.Body.Write(msg, 0, msg.Length); - response.Send(); - return true; - } - - // Check the hostnames we always allow - if (DEFAULT_ALLOWED.Contains(host, StringComparer.OrdinalIgnoreCase)) - return false; - - // Then the user specified ones - if (h != null && h.Contains(host, StringComparer.OrdinalIgnoreCase)) - return false; - - // Disable checks if we have an asterisk - if (h != null && Array.IndexOf(h, "*") >= 0) - return false; - - // Finally, check if we have a potential IP address - var v4 = IPV4.Match(host); - var v6 = IPV6.Match(host); - - if ((v4.Success && v4.Length == host.Length) || (v6.Success && v6.Length == host.Length)) - { - try - { - // Verify that the hostname is indeed a valid IP address - System.Net.IPAddress.Parse(host); - return false; - } - catch - { } - } - - // Failed to find a valid header - response.Reason = $"The host header sent by the client is not allowed"; - response.Status = System.Net.HttpStatusCode.Forbidden; - var txt = System.Text.Encoding.ASCII.GetBytes(response.Reason); - response.ContentType = "text/plain"; - response.ContentLength = txt.Length; - response.Body.Write(txt, 0, txt.Length); - response.Send(); - return true; - - } - } - } -} diff --git a/Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs b/Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs deleted file mode 100644 index cf16695e5..000000000 --- a/Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs +++ /dev/null @@ -1,293 +0,0 @@ -// 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; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using HttpServer.HttpModules; - -namespace Duplicati.Server.WebServer -{ - /// - /// Helper class for enforcing the built-in authentication on Synology DSM - /// - public class SynologyAuthenticationHandler : HttpModule - { - /// - /// The path to the login.cgi script - /// - private readonly string LOGIN_CGI = GetEnvArg("SYNO_LOGIN_CGI", "/usr/syno/synoman/webman/login.cgi"); - /// - /// The path to the authenticate.cgi script - /// - private readonly string AUTH_CGI = GetEnvArg("SYNO_AUTHENTICATE_CGI", "/usr/syno/synoman/webman/modules/authenticate.cgi"); - /// - /// A flag indicating if only admins are allowed - /// - private readonly bool ADMIN_ONLY = !(GetEnvArg("SYNO_ALL_USERS", "0") == "1"); - /// - /// A flag indicating if the XSRF token should be fetched automatically - /// - private readonly bool AUTO_XSRF = GetEnvArg("SYNO_AUTO_XSRF", "1") == "1"; - - /// - /// A flag indicating that the auth-module is fully disabled - /// - private readonly bool FULLY_DISABLED; - - /// - /// Re-evaluate the logins periodically to ensure it is still valid - /// - private readonly TimeSpan CACHE_TIMEOUT = TimeSpan.FromMinutes(3); - - /// - /// A cache of previously authenticated logins - /// - private readonly ConcurrentDictionary m_logincache = new ConcurrentDictionary(); - - /// - /// Initializes a new instance of the class. - /// - public SynologyAuthenticationHandler() - { - Console.WriteLine("Enabling Synology integrated authentication handler"); - var disable = false; - if (!File.Exists(LOGIN_CGI)) - { - Console.WriteLine("Disabling webserver as the login script is not found: {0}", LOGIN_CGI); - disable = true; - } - if (!File.Exists(AUTH_CGI)) - { - Console.WriteLine("Disabling webserver as the auth script is not found: {0}", AUTH_CGI); - disable = true; - } - - FULLY_DISABLED = disable; - } - - /// - /// Processes the request - /// - /// true if the request is handled false otherwise. - /// The request. - /// The response. - /// The session. - public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session) - { - if (FULLY_DISABLED) - { - response.Status = System.Net.HttpStatusCode.ServiceUnavailable; - response.Reason = "The system is incorrectly configured"; - return true; - } - - var limitedAccess = - request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase) - || - request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGIN_SCRIPT_URI, StringComparison.OrdinalIgnoreCase) - || - request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGOUT_SCRIPT_URI, StringComparison.OrdinalIgnoreCase); - - if (!limitedAccess) - return false; - - var tmpenv = new Dictionary(); - - tmpenv["REMOTE_ADDR"] = request.RemoteEndPoint.Address.ToString(); - tmpenv["REMOTE_PORT"] = request.RemoteEndPoint.Port.ToString(); - - if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"])) - tmpenv["REMOTE_ADDR"] = request.Headers["X-Real-IP"]; - if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"])) - tmpenv["REMOTE_PORT"] = request.Headers["X-Real-Port"]; - - var loginid = request.Cookies["id"]?.Value; - if (!string.IsNullOrWhiteSpace(loginid)) - tmpenv["HTTP_COOKIE"] = "id=" + loginid; - - var xsrftoken = request.Headers["X-Syno-Token"]; - if (string.IsNullOrWhiteSpace(xsrftoken)) - xsrftoken = request.QueryString["SynoToken"]?.Value; - - var cachestring = BuildCacheKey(tmpenv, xsrftoken); - - DateTime cacheExpires; - if (m_logincache.TryGetValue(cachestring, out cacheExpires) && cacheExpires > DateTime.Now) - { - // We do not refresh the cache, as we need to ask the synology auth system periodically - return false; - } - - if (string.IsNullOrWhiteSpace(xsrftoken) && AUTO_XSRF) - { - var authre = new Regex(@"""SynoToken""\s?\:\s?""(?[^""]+)"""); - try - { - var resp = ShellExec(LOGIN_CGI, env: tmpenv).Result; - - var m = authre.Match(resp); - if (m.Success) - xsrftoken = m.Groups["token"].Value; - else - throw new Exception("Unable to get XSRF token"); - } - catch (Exception) - { - response.Status = System.Net.HttpStatusCode.InternalServerError; - response.Reason = "The system is incorrectly configured"; - return true; - - } - } - - if (!string.IsNullOrWhiteSpace(xsrftoken)) - tmpenv["HTTP_X_SYNO_TOKEN"] = xsrftoken; - - cachestring = BuildCacheKey(tmpenv, xsrftoken); - - var username = GetEnvArg("SYNO_USERNAME"); - if (string.IsNullOrWhiteSpace(username)) - { - try - { - username = ShellExec(AUTH_CGI, shell: false, exitcode: 0, env: tmpenv).Result; - } - catch (Exception) - { - response.Status = System.Net.HttpStatusCode.InternalServerError; - response.Reason = "The system is incorrectly configured"; - return true; - } - } - - if (string.IsNullOrWhiteSpace(username)) - { - response.Status = System.Net.HttpStatusCode.Forbidden; - response.Reason = "Permission denied, not logged in"; - return true; - } - - username = username.Trim(); - - if (ADMIN_ONLY) - { - var groups = GetEnvArg("SYNO_GROUP_IDS"); - - if (string.IsNullOrWhiteSpace(groups)) - { - groups = ShellExec("id", "-G '" + username.Trim().Replace("'", "\\'") + "'", exitcode: 0).Result ?? string.Empty; - groups = groups.Replace(Environment.NewLine, String.Empty); - } - - if (!groups.Split(new char[] { ' ' }).Contains("101")) - { - response.Status = System.Net.HttpStatusCode.Forbidden; - response.Reason = "Administrator login required"; - return true; - } - } - - // We are now authenticated, add to cache - m_logincache[cachestring] = DateTime.Now + CACHE_TIMEOUT; - return false; - } - - /// - /// Builds a cache key from the environment data - /// - /// The cache key. - /// The environment. - /// The XSRF token. - private static string BuildCacheKey(Dictionary values, string xsrftoken) - { - if (!values.ContainsKey("REMOTE_ADDR") || !values.ContainsKey("REMOTE_PORT") || !values.ContainsKey("HTTP_COOKIE")) - return null; - - return string.Format("{0}:{1}/{2}?{3}", values["REMOTE_ADDR"], values["REMOTE_PORT"], values["HTTP_COOKIE"], xsrftoken); - } - - /// - /// Runs an external command - /// - /// The stdout data. - /// The executable - /// The executable and the arguments. - /// If set to true use the shell context for execution. - /// Set the value to check for a particular exitcode. - private static async Task ShellExec(string command, string args = null, bool shell = false, int exitcode = -1, Dictionary env = null) - { - var psi = new ProcessStartInfo() - { - FileName = command, - Arguments = shell ? null : args, - UseShellExecute = false, - RedirectStandardInput = shell, - RedirectStandardOutput = true, - RedirectStandardError = false - }; - - if (env != null) - foreach (var pk in env) - psi.EnvironmentVariables[pk.Key] = pk.Value; - - using (var p = System.Diagnostics.Process.Start(psi)) - { - if (shell && args != null) - await p.StandardInput.WriteLineAsync(args); - - var res = p.StandardOutput.ReadToEndAsync(); - - var tries = 10; - var ms = (int)TimeSpan.FromSeconds(0.5).TotalMilliseconds; - while (tries > 0 && !p.HasExited) - { - tries--; - p.WaitForExit(ms); - } - - if (!p.HasExited) - try { p.Kill(); } - catch { } - - if (!p.HasExited || (p.ExitCode != exitcode && exitcode != -1)) - throw new Exception(string.Format("Exit code was: {0}, stdout: {1}", p.ExitCode, res)); - return await res; - } - } - - /// - /// Gets the environment variable argument. - /// - /// The environment variable. - /// The name of the environment variable. - /// The default value. - private static string GetEnvArg(string key, string @default = null) - { - var res = Environment.GetEnvironmentVariable(key); - return string.IsNullOrWhiteSpace(res) ? @default : res.Trim(); - } - } -} diff --git a/Duplicati.Library.RestAPI/newbackup.json b/Duplicati.Library.RestAPI/newbackup.json index 3fbb3c65b..0053b721c 100644 --- a/Duplicati.Library.RestAPI/newbackup.json +++ b/Duplicati.Library.RestAPI/newbackup.json @@ -1,15 +1,15 @@ { - Backup: { - Settings: [ - {Name: "encryption-module", Value: "aes"}, - {Name: "compression-module", Value: "zip"}, - {Name: "dblock-size", Value: "50mb"}, - {Name: "keep-time", Value: ""} - ] - }, - Schedule: { - Repeat: "1D", - Time: "13:00:00", - AllowedDays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] - } -} \ No newline at end of file + "Backup": { + "Settings": [ + { "Name": "encryption-module", "Value": "aes" }, + { "Name": "compression-module", "Value": "zip" }, + { "Name": "dblock-size", "Value": "50mb" }, + { "Name": "keep-time", "Value": "" } + ] + }, + "Schedule": { + "Repeat": "1D", + "Time": "13:00:00", + "AllowedDays": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] + } +} diff --git a/Duplicati/CommandLine/ConfigurationImporter/Program.cs b/Duplicati/CommandLine/ConfigurationImporter/Program.cs index 685d1a81e..8ed4f0c0d 100644 --- a/Duplicati/CommandLine/ConfigurationImporter/Program.cs +++ b/Duplicati/CommandLine/ConfigurationImporter/Program.cs @@ -20,8 +20,8 @@ // DEALINGS IN THE SOFTWARE. using Duplicati.Library.AutoUpdater; +using Duplicati.Library.RestAPI; using Duplicati.Server.Serializable; -using Duplicati.Server.WebServer.RESTMethods; using System; using System.Collections.Generic; using System.Linq; @@ -63,7 +63,7 @@ namespace Duplicati.CommandLine.ConfigurationImporter { "server-datafolder", serverDatafolder } }; - ImportExportStructure importedStructure = Backups.ImportBackup(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: "), advancedOptions); + ImportExportStructure importedStructure = BackupImportExportHandler.ImportBackup(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: "), advancedOptions); Console.WriteLine($"Imported \"{importedStructure.Backup.Name}\" with ID {importedStructure.Backup.ID} and local database at {importedStructure.Backup.DBPath}."); return 0; diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs index 8097d56fb..703b2b28b 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs @@ -25,7 +25,7 @@ using System.Linq; using System.Text; namespace Duplicati.GUI.TrayIcon -{ +{ /// /// We keep the hosted instance here to allow the TrayIcon executable to load without requiring the Server.exe /// @@ -37,19 +37,24 @@ namespace Duplicati.GUI.TrayIcon public HostedInstanceKeeper(string[] args) { - m_runner = new System.Threading.Thread((dummy_arg) => { + m_runner = new System.Threading.Thread((dummy_arg) => + { try { //When running the hosted instance we do not really care what port we are using, // so we just throw a few out there and try them - if (args == null || !args.Any(x => x.Trim().StartsWith("--" + Duplicati.Server.WebServer.Server.OPTION_PORT + "=", StringComparison.OrdinalIgnoreCase))) - args = (args ?? new string[0]).Union(new string[] { "--" + Duplicati.Server.WebServer.Server.OPTION_PORT + "=8200,8300,8400,8500,8600,8700,8800,8900,8989" }).ToArray(); - + if (args == null || !args.Any(x => x.Trim().StartsWith("--" + Duplicati.Server.WebServerLoader.OPTION_PORT + "=", StringComparison.OrdinalIgnoreCase))) + args = (args ?? new string[0]).Union(new string[] { "--" + Duplicati.Server.WebServerLoader.OPTION_PORT + "=8200,8300,8400,8500,8600,8700,8800,8900,8989" }).ToArray(); + Duplicati.Server.Program.Main(args); - } catch (Exception ex) { + } + catch (Exception ex) + { m_runnerException = ex; Duplicati.Server.Program.ServerStartedEvent.Set(); - } finally { + } + finally + { if (InstanceShutdown != null) try { InstanceShutdown(); } catch (Exception shutex) @@ -62,7 +67,7 @@ namespace Duplicati.GUI.TrayIcon } }); - + m_runner.Start(); if (!Duplicati.Server.Program.ServerStartedEvent.WaitOne(TimeSpan.FromSeconds(100), true)) diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs index 8bf59d62e..7d67b246b 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs @@ -36,7 +36,7 @@ namespace Duplicati.GUI.TrayIcon } public static HttpServerConnection Connection; - + private const string HOSTURL_OPTION = "hosturl"; private const string NOHOSTEDSERVER_OPTION = "no-hosted-server"; private const string READCONFIGFROMDB_OPTION = "read-config-from-db"; @@ -45,7 +45,7 @@ namespace Duplicati.GUI.TrayIcon private const string BROWSER_COMMAND_OPTION = "browser-command"; private const string DEFAULT_HOSTURL = "http://localhost:8200"; - + private static string _browser_command = null; private static bool disableTrayIconLogin = false; private static bool openui = false; @@ -134,7 +134,7 @@ namespace Duplicati.GUI.TrayIcon } else if (Library.Utility.Utility.ParseBoolOption(options, READCONFIGFROMDB_OPTION)) { - databaseConnection = Server.Program.GetDatabaseConnection(null, options); + databaseConnection = Server.Program.GetDatabaseConnection(options); if (databaseConnection != null) { @@ -261,7 +261,7 @@ namespace Duplicati.GUI.TrayIcon { new Duplicati.Library.Interface.CommandLineArgument(HOSTURL_OPTION, CommandLineArgument.ArgumentType.String, "Selects the url to connect to", "Supply the url that the TrayIcon will connect to and show status for", DEFAULT_HOSTURL), new Duplicati.Library.Interface.CommandLineArgument(NOHOSTEDSERVER_OPTION, CommandLineArgument.ArgumentType.String, "Disables local server", "Set this option to not spawn a local service, use if the TrayIcon should connect to a running service"), - new Duplicati.Library.Interface.CommandLineArgument(READCONFIGFROMDB_OPTION, CommandLineArgument.ArgumentType.String, "Read server connection info from DB", $"Set this option to read server connection info for running service from its database (only together with {NOHOSTEDSERVER_OPTION})"), + new Duplicati.Library.Interface.CommandLineArgument(READCONFIGFROMDB_OPTION, CommandLineArgument.ArgumentType.String, "Read server connection info from DB", $"Set this option to read server connection info for running service from its database (only together with {NOHOSTEDSERVER_OPTION})"), new Duplicati.Library.Interface.CommandLineArgument(BROWSER_COMMAND_OPTION, CommandLineArgument.ArgumentType.String, "Sets the browser command", "Set this option to override the default browser detection"), }; diff --git a/Duplicati/Library/Utility/Abstractions/IBoolParser.cs b/Duplicati/Library/Utility/Abstractions/IBoolParser.cs deleted file mode 100644 index 782b80a76..000000000 --- a/Duplicati/Library/Utility/Abstractions/IBoolParser.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Duplicati.Library.Utility.Abstractions; - -public interface IBoolParser -{ - bool ParseBool(string value, bool @default = false); -} \ No newline at end of file diff --git a/Duplicati/Library/Utility/BoolParser.cs b/Duplicati/Library/Utility/BoolParser.cs deleted file mode 100644 index 1f4f0d757..000000000 --- a/Duplicati/Library/Utility/BoolParser.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Duplicati.Library.Utility.Abstractions; - -namespace Duplicati.Library.Utility; - -public class BoolParser : IBoolParser -{ - public bool ParseBool(string value, bool @default = false) - { - return Utility.ParseBool(value, @default); - } -} \ No newline at end of file diff --git a/Duplicati/Library/Utility/WorkerThread.cs b/Duplicati/Library/Utility/WorkerThread.cs index 267b40e89..eaf49d5a1 100644 --- a/Duplicati/Library/Utility/WorkerThread.cs +++ b/Duplicati/Library/Utility/WorkerThread.cs @@ -24,6 +24,10 @@ using System.Collections.Generic; using System.Text; using System.Threading; + +// TODO: Delete this class. +// It is essentially a queue that is processed by a worker thread, and can be implemented using a BlockingCollection or similar. + namespace Duplicati.Library.Utility { /// @@ -134,7 +138,7 @@ namespace Duplicati.Library.Utility { get { - lock(m_lock) + lock (m_lock) return new List(m_tasks); } @@ -174,7 +178,8 @@ namespace Duplicati.Library.Utility /// If set to true skip queue. public void AddTask(Tx task, bool skipQueue) { - if (!skipQueue) { + if (!skipQueue) + { // Fall back to default AddTask method AddTask(task); return; @@ -280,7 +285,7 @@ namespace Duplicati.Library.Utility { m_currentTask = null; - lock(m_lock) + lock (m_lock) if (m_state == WorkerThread.RunState.Run && m_tasks.Count > 0) m_currentTask = m_tasks.Dequeue(); @@ -311,7 +316,7 @@ namespace Duplicati.Library.Utility return; if (m_currentTask == null && m_state == WorkerThread.RunState.Run) - lock(m_lock) + lock (m_lock) if (m_tasks.Count > 0) m_currentTask = m_tasks.Dequeue(); @@ -348,7 +353,7 @@ namespace Duplicati.Library.Utility if (CompletedWork != null) try { CompletedWork(this, task); } - catch (Exception ex) + catch (Exception ex) { try { OnError(this, task, ex); } catch { } diff --git a/Duplicati/License/Duplicati.License.csproj b/Duplicati/License/Duplicati.License.csproj index 74e5e528a..c65580a7a 100644 --- a/Duplicati/License/Duplicati.License.csproj +++ b/Duplicati/License/Duplicati.License.csproj @@ -83,7 +83,7 @@ licenses\license.txt PreserveNewest - + PreserveNewest @@ -126,18 +126,6 @@ licenses\gpg\licensedata.json PreserveNewest - - licenses\HttpServer\licensedata.json - PreserveNewest - - - licenses\HttpServer\Homepage.txt - PreserveNewest - - - licenses\HttpServer\license.txt - PreserveNewest - licenses\Json.NET\licensedata.json PreserveNewest diff --git a/Duplicati/License/LicenseReader.cs b/Duplicati/License/LicenseReader.cs index e46245276..b96f67f54 100644 --- a/Duplicati/License/LicenseReader.cs +++ b/Duplicati/License/LicenseReader.cs @@ -1,30 +1,28 @@ -// 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. +// 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; using System.Collections.Generic; -using System.Text; -using Duplicati.Library.Common.IO; -using Duplicati.Library.Utility; - +using Duplicati.Library.Common.IO; + namespace Duplicati.License { /// @@ -52,14 +50,14 @@ namespace Duplicati.License /// A list of licenses public static List ReadLicenses(string basefolder) { - List res = new List(); + var res = new List(); - string[] folders = SystemIO.IO_OS.GetDirectories(basefolder); + var folders = SystemIO.IO_OS.GetDirectories(basefolder); Array.Sort(folders); foreach (string folder in folders) { - string name = System.IO.Path.GetFileName(folder); + var name = System.IO.Path.GetFileName(folder); string urlfile = null; string licensefile = null; string jsonfile = null; diff --git a/Duplicati/License/duplicati-url.txt b/Duplicati/License/duplicati-url.txt new file mode 100644 index 000000000..a13a62c15 --- /dev/null +++ b/Duplicati/License/duplicati-url.txt @@ -0,0 +1 @@ + https://duplicati.com \ No newline at end of file diff --git a/Duplicati/License/licenses/MailKit/Homepage.txt b/Duplicati/License/licenses/MailKit/Homepage.txt deleted file mode 100644 index ae7b434d9..000000000 --- a/Duplicati/License/licenses/MailKit/Homepage.txt +++ /dev/null @@ -1 +0,0 @@ -https://github.com/jstedfast/MailKit \ No newline at end of file diff --git a/Duplicati/License/licenses/MailKit/license.txt b/Duplicati/License/licenses/MailKit/license.txt deleted file mode 100644 index 4dbb40bbf..000000000 --- a/Duplicati/License/licenses/MailKit/license.txt +++ /dev/null @@ -1,21 +0,0 @@ -## License Information - -MailKit is Copyright (C) 2013-2016 Xamarin Inc. and is licensed under the MIT license: - - 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. \ No newline at end of file diff --git a/Duplicati/License/licenses/MailKit/licensedata.json b/Duplicati/License/licenses/MailKit/licensedata.json deleted file mode 100644 index 829027ee3..000000000 --- a/Duplicati/License/licenses/MailKit/licensedata.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "MailKit", - "description": "A cross-platform .NET library for IMAP, POP3, and SMTP", - "link": "https://github.com/jstedfast/MailKit/", - "license": "MIT license", - "notes": "" -} \ No newline at end of file diff --git a/Duplicati/License/licenses/duplicati-url.txt b/Duplicati/License/licenses/duplicati-url.txt deleted file mode 100644 index 0a998abd0..000000000 --- a/Duplicati/License/licenses/duplicati-url.txt +++ /dev/null @@ -1 +0,0 @@ - http://www.duplicati.com \ No newline at end of file diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs b/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs index 3a4109bd7..19915ed9f 100755 --- a/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs @@ -1,55 +1,27 @@ -// 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. +// 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; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace Duplicati.Server.Serialization { - public enum RunnerState - { - Started, - Suspended, - Running, - Stopped, - } - - public enum RunnerResult - { - OK, - Partial, - Warning, - Error - } - - public enum CloseReason - { - None, - ApplicationExitCall, - TaskManagerClosing, - WindowsShutDown, - UserClosing - } - public enum LiveControlState { Running, diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IBackup.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IBackup.cs index 0d3dcd555..de6f3aaaf 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IBackup.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IBackup.cs @@ -19,7 +19,6 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; namespace Duplicati.Server.Serialization.Interface @@ -53,27 +52,27 @@ namespace Duplicati.Server.Serialization.Interface /// The path to the local database /// string DBPath { get; } - + /// /// The backup source folders and files /// string[] Sources { get; set; } - + /// /// The backup settings /// ISetting[] Settings { get; set; } - + /// /// The filters applied to the source files /// IFilter[] Filters { get; set; } - + /// /// The backup metadata /// IDictionary Metadata { get; set; } - + /// /// Gets a value indicating if this instance is not persisted to the database /// diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IDynamicModule.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IDynamicModule.cs index 46ed948c0..5cf0fc9d5 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IDynamicModule.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IDynamicModule.cs @@ -19,9 +19,6 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; -using System.Collections.Generic; - namespace Duplicati.Server.Serialization.Interface { /// diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IFilter.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IFilter.cs index 8f236be9e..671b47f3f 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IFilter.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IFilter.cs @@ -19,8 +19,6 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; - namespace Duplicati.Server.Serialization.Interface { /// @@ -32,12 +30,12 @@ namespace Duplicati.Server.Serialization.Interface /// The sort order /// long Order { get; set; } - + /// /// True if the filter includes the items, false if it excludes /// bool Include { get; set; } - + /// /// The filter expression. /// If the filter is a regular expression, it starts and ends with hard brackets [ ] diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IProgressEventData.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IProgressEventData.cs index 870b86c7a..1b04aa5ea 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IProgressEventData.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IProgressEventData.cs @@ -1,27 +1,23 @@ -// 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; -using System.Collections.Generic; -using System.Linq; -using System.Text; +// 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. namespace Duplicati.Server.Serialization.Interface { @@ -29,19 +25,19 @@ namespace Duplicati.Server.Serialization.Interface { string BackupID { get; } long TaskID { get; } - - string BackendAction { get; } + + string BackendAction { get; } string BackendPath { get; } long BackendFileSize { get; } long BackendFileProgress { get; } long BackendSpeed { get; } bool BackendIsBlocking { get; } - + string CurrentFilename { get; } long CurrentFilesize { get; } long CurrentFileoffset { get; } bool CurrentFilecomplete { get; } - + string Phase { get; } float OverallProgress { get; } long ProcessedFileCount { get; } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IQueuedTask.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IQueuedTask.cs index bdac12250..e49739d39 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IQueuedTask.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IQueuedTask.cs @@ -19,8 +19,6 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; - namespace Duplicati.Server.Serialization.Interface { public interface IQueuedTask @@ -28,7 +26,7 @@ namespace Duplicati.Server.Serialization.Interface long TaskID { get; } string BackupID { get; } Duplicati.Server.Serialization.DuplicatiOperation Operation { get; } - + } } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IServerSettings.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IServerSettings.cs index ed62731bb..74bacb603 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IServerSettings.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IServerSettings.cs @@ -19,8 +19,6 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; - namespace Duplicati.Server.Serialization.Interface { /// @@ -44,7 +42,7 @@ namespace Duplicati.Server.Serialization.Interface /// The generic modules known by the server /// IDynamicModule[] GenericModules { get; } - + /// /// The filters that are applied to all backups /// diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/ISetting.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/ISetting.cs index 63729bd42..fdcf2ef42 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/ISetting.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/ISetting.cs @@ -19,8 +19,6 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; - namespace Duplicati.Server.Serialization.Interface { /// @@ -40,12 +38,12 @@ namespace Duplicati.Server.Serialization.Interface /// The setting value /// string Value { get; set; } - + /// /// The actual option arguments /// - Duplicati.Library.Interface.ICommandLineArgument Argument { get; } - + Library.Interface.ICommandLineArgument Argument { get; } + } } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/InterfaceResolver.cs b/Duplicati/Server/Duplicati.Server.Serialization/InterfaceResolver.cs index a6c500658..9e6588f89 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/InterfaceResolver.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/InterfaceResolver.cs @@ -1,27 +1,24 @@ -// 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. +// 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; -using System.Collections.Generic; -using System.Linq; -using System.Text; using Newtonsoft.Json.Converters; using Newtonsoft.Json; @@ -106,12 +103,12 @@ namespace Duplicati.Server.Serialization throw new JsonSerializationException(string.Format("Cannot convert null value to {0}", objectType)); else if (reader.TokenType != JsonToken.String) throw new JsonSerializationException(string.Format("Cannot convert {0} value to {1}", reader.TokenType, objectType)); - + var v = (string)reader.Value; DayOfWeek result; if (Enum.TryParse(v, out result)) return result; - + switch (v.ToLowerInvariant()) { case "mon": @@ -127,9 +124,9 @@ namespace Duplicati.Server.Serialization case "sat": return DayOfWeek.Saturday; case "sun": - return DayOfWeek.Sunday; + return DayOfWeek.Sunday; } - + throw new JsonSerializationException(string.Format("Cannot convert \"{0}\" to {1}", v, objectType)); } @@ -140,5 +137,5 @@ namespace Duplicati.Server.Serialization #endregion } - + } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs b/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs index 7f3283790..2af57ca0a 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs @@ -18,18 +18,12 @@ // 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; -using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; namespace Duplicati.Server.Serialization -{ +{ public class Serializer { public static JsonSerializerSettings JsonSettings { get; } @@ -54,7 +48,7 @@ namespace Duplicati.Server.Serialization public static void SerializeJson(System.IO.TextWriter sw, object o, bool preventDispose = false) { - Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(JsonSettings); + JsonSerializer jsonSerializer = JsonSerializer.Create(JsonSettings); var jsonWriter = new JsonTextWriter(sw); using (preventDispose ? null : jsonWriter) { @@ -65,26 +59,9 @@ namespace Duplicati.Server.Serialization } } - public static async Task SerializeJsonAsync(System.IO.TextWriter tw, object o, bool preventDispose = false) - { - Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(JsonSettings); - StringBuilder sb = new StringBuilder(); - StringWriter sw = new StringWriter(sb); - var jsonWriter = new JsonTextWriter(sw); - using (preventDispose ? null : jsonWriter) - { - jsonWriter.Formatting = m_jsonFormatting; - jsonSerializer.Serialize(jsonWriter, o); - await jsonWriter.FlushAsync(); - } - await sw.FlushAsync(); - await tw.WriteAsync(sb.ToString()); - await tw.FlushAsync(); - } - public static T Deserialize(System.IO.TextReader sr) { - Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(JsonSettings); + JsonSerializer jsonSerializer = JsonSerializer.Create(JsonSettings); using (var jsonReader = new JsonTextReader(sr)) { jsonReader.Culture = System.Globalization.CultureInfo.InvariantCulture; diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index ed3cfc548..52ecaad8b 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -22,15 +22,13 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading.Tasks; using Duplicati.Library.Common; using Duplicati.Library.Common.IO; -using Duplicati.Library.IO; using Duplicati.Library.RestAPI; using Duplicati.WebserverCore; using Duplicati.WebserverCore.Abstractions; -using Duplicati.WebserverCore.Database; using Microsoft.Extensions.DependencyInjection; -using Sharp.Xmpp.Extensions.Dataforms; namespace Duplicati.Server { @@ -108,18 +106,13 @@ namespace Duplicati.Server /// /// The controller interface for pause/resume and throttle options /// - public static LiveControls LiveControl { get => DuplicatiWebserver.Provider.GetRequiredService() ; } + public static LiveControls LiveControl { get => DuplicatiWebserver.Provider.GetRequiredService(); } /// /// The application exit event /// public static System.Threading.ManualResetEvent ApplicationExitEvent { get => FIXMEGlobal.ApplicationExitEvent; set => FIXMEGlobal.ApplicationExitEvent = value; } - /// - /// The webserver instance - /// - private static WebServer.Server WebServer; - /// /// Duplicati webserver instance /// @@ -169,7 +162,7 @@ namespace Duplicati.Server { get { - return WebServer.Port; + return DuplicatiWebserver.Port; } } @@ -252,10 +245,7 @@ namespace Duplicati.Server try { - DuplicatiWebserver = new DuplicatiWebserver(); - DuplicatiWebserver.InitWebServer(); - FIXMEGlobal.Provider = DuplicatiWebserver.Provider; - DataConnection = GetDatabaseConnection(DuplicatiWebserver, commandlineOptions); + DataConnection = GetDatabaseConnection(commandlineOptions); if (!DataConnection.ApplicationSettings.FixedInvalidBackupId) DataConnection.FixInvalidBackupId(); @@ -272,7 +262,9 @@ namespace Duplicati.Server { DataConnection.LogError(null, "Error in updater", obj); }; - + + DuplicatiWebserver = StartWebServer(commandlineOptions).ConfigureAwait(false).GetAwaiter().GetResult(); + UpdatePoller.Init(); SetPurgeTempFilesTimer(commandlineOptions); @@ -281,11 +273,10 @@ namespace Duplicati.Server SetWorkerThread(); - StartWebServer(DuplicatiWebserver, commandlineOptions); if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, "ping-pong-keepalive")) { - PingPongThread = new System.Threading.Thread(PingPongMethod) {IsBackground = true}; + PingPongThread = new System.Threading.Thread(PingPongMethod) { IsBackground = true }; PingPongThread.Start(); } @@ -296,7 +287,7 @@ namespace Duplicati.Server { System.Diagnostics.Trace.WriteLine(Strings.Program.SeriousError(mex.ToString())); if (!writeToConsole) throw; - + Console.WriteLine(Strings.Program.SeriousError(mex.ToString())); return 100; } @@ -334,14 +325,31 @@ namespace Duplicati.Server return 0; } - private static void StartWebServer(DuplicatiWebserver webserver, Dictionary commandlineOptions) + private static async Task StartWebServer(IReadOnlyDictionary options) { - WebServer = new WebServer.Server(commandlineOptions); + var server = await WebServerLoader.TryRunServer(options, async parsedOptions => + { + var mappedSettings = new DuplicatiWebserver.InitSettings( + parsedOptions.WebRoot, + parsedOptions.Port, + parsedOptions.Interface, + parsedOptions.Certificate, + parsedOptions.Servername, + parsedOptions.Password, + parsedOptions.AllowedHostnames); - ServerPortChanged |= WebServer.Port != DataConnection.ApplicationSettings.LastWebserverPort; - DataConnection.ApplicationSettings.LastWebserverPort = WebServer.Port; - - webserver.Start().GetAwaiter().GetResult(); + var server = new DuplicatiWebserver(); + + server.InitWebServer(mappedSettings, DataConnection); + server.Start(mappedSettings); + return server; + }).ConfigureAwait(false); + + FIXMEGlobal.Provider = server.Provider; + ServerPortChanged |= server.Port != DataConnection.ApplicationSettings.LastWebserverPort; + DataConnection.ApplicationSettings.LastWebserverPort = server.Port; + + return server; } private static void SetWorkerThread() @@ -350,7 +358,7 @@ namespace Duplicati.Server FIXMEGlobal.WorkThread.StartingWork += (worker, task) => { SignalNewEvent(null, null); }; FIXMEGlobal.WorkThread.CompletedWork += (worker, task) => { SignalNewEvent(null, null); }; FIXMEGlobal.WorkThread.WorkQueueChanged += (worker) => { SignalNewEvent(null, null); }; - FIXMEGlobal.Scheduler.NewSchedule += new EventHandler(SignalNewEvent); + FIXMEGlobal.Scheduler.SubScribeToNewSchedule(() => SignalNewEvent(null, null)); FIXMEGlobal.WorkThread.OnError += (worker, task, exception) => { Program.DataConnection.LogError(task?.BackupID, "Error in worker", exception); @@ -471,16 +479,16 @@ namespace Duplicati.Server private static void AdjustApplicationSettings(Dictionary commandlineOptions) { - if (commandlineOptions.ContainsKey("webservice-password")) + if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_PASSWORD)) { - DataConnection.ApplicationSettings.SetWebserverPassword(commandlineOptions["webservice-password"]); + DataConnection.ApplicationSettings.SetWebserverPassword(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_PASSWORD]); } DataConnection.ApplicationSettings.GenerateWebserverPasswordTrayIcon(); - if (commandlineOptions.ContainsKey("webservice-allowed-hostnames")) + if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES)) { - DataConnection.ApplicationSettings.SetAllowedHostnames(commandlineOptions["webservice-allowed-hostnames"]); + DataConnection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES]); } } @@ -559,7 +567,7 @@ namespace Duplicati.Server throw new Exception("Server invoked with --help"); } - public static Database.Connection GetDatabaseConnection(object webserver, Dictionary commandlineOptions) + public static Database.Connection GetDatabaseConnection(Dictionary commandlineOptions) { var serverDataFolder = Environment.GetEnvironmentVariable(DATAFOLDER_ENV_NAME); if (commandlineOptions.ContainsKey("server-datafolder")) @@ -698,7 +706,7 @@ namespace Duplicati.Server { case LiveControls.LiveControlState.Paused: { - worker.Pause(); + worker.Pause(); var t = worker.CurrentTask; t?.Pause(); break; @@ -757,26 +765,22 @@ namespace Duplicati.Server new Duplicati.Library.Interface.CommandLineArgument("tempdir", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.TempdirShort, Strings.Program.TempdirLong, System.IO.Path.GetTempPath()), new Duplicati.Library.Interface.CommandLineArgument("help", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.HelpCommandDescription, Strings.Program.HelpCommandDescription), new Duplicati.Library.Interface.CommandLineArgument("parameters-file", Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.ParametersFileOptionShort, Strings.Program.ParametersFileOptionLong2, "", new string[] {"parameter-file", "parameterfile"}), - new Duplicati.Library.Interface.CommandLineArgument("unencrypted-database", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.UnencrypteddatabaseCommandDescription, Strings.Program.UnencrypteddatabaseCommandDescription, "false", null, null, "Database encryption is no longer supported, this setting has no effect"), new Duplicati.Library.Interface.CommandLineArgument("portable-mode", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.PortablemodeCommandDescription, Strings.Program.PortablemodeCommandDescription), new Duplicati.Library.Interface.CommandLineArgument("log-file", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.LogfileCommandDescription, Strings.Program.LogfileCommandDescription), new Duplicati.Library.Interface.CommandLineArgument("log-level", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Enumeration, Strings.Program.LoglevelCommandDescription, Strings.Program.LoglevelCommandDescription, "Warning", null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType))), - new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_WEBROOT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverWebrootDescription, Strings.Program.WebserverWebrootDescription, Duplicati.Server.WebServer.Server.DEFAULT_OPTION_WEBROOT), - new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_PORT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPortDescription, Strings.Program.WebserverPortDescription, Duplicati.Server.WebServer.Server.DEFAULT_OPTION_PORT.ToString()), - new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_SSLCERTIFICATEFILE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificateFileDescription, Strings.Program.WebserverCertificateFileDescription, Duplicati.Server.WebServer.Server.OPTION_SSLCERTIFICATEFILE), - new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_SSLCERTIFICATEFILEPASSWORD, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificatePasswordDescription, Strings.Program.WebserverCertificatePasswordDescription, Duplicati.Server.WebServer.Server.OPTION_SSLCERTIFICATEFILEPASSWORD), - new Duplicati.Library.Interface.CommandLineArgument(Duplicati.Server.WebServer.Server.OPTION_INTERFACE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverInterfaceDescription, Strings.Program.WebserverInterfaceDescription, Duplicati.Server.WebServer.Server.DEFAULT_OPTION_INTERFACE), - new Duplicati.Library.Interface.CommandLineArgument("webservice-password", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password, Strings.Program.WebserverPasswordDescription, Strings.Program.WebserverPasswordDescription), - new Duplicati.Library.Interface.CommandLineArgument("webservice-allowed-hostnames", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverAllowedhostnamesDescription, Strings.Program.WebserverAllowedhostnamesDescription), + new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBROOT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverWebrootDescription, Strings.Program.WebserverWebrootDescription, WebServerLoader.DEFAULT_OPTION_WEBROOT), + new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_PORT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPortDescription, Strings.Program.WebserverPortDescription, WebServerLoader.DEFAULT_OPTION_PORT.ToString()), + new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_SSLCERTIFICATEFILE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificateFileDescription, Strings.Program.WebserverCertificateFileDescription, WebServerLoader.OPTION_SSLCERTIFICATEFILE), + new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_SSLCERTIFICATEFILEPASSWORD, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificatePasswordDescription, Strings.Program.WebserverCertificatePasswordDescription, WebServerLoader.OPTION_SSLCERTIFICATEFILEPASSWORD), + new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_INTERFACE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverInterfaceDescription, Strings.Program.WebserverInterfaceDescription, WebServerLoader.DEFAULT_OPTION_INTERFACE), + new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password, Strings.Program.WebserverPasswordDescription, Strings.Program.WebserverPasswordDescription), + new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverAllowedhostnamesDescription, Strings.Program.WebserverAllowedhostnamesDescription), new Duplicati.Library.Interface.CommandLineArgument("ping-pong-keepalive", 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)), }); - if (!Platform.IsClientPosix) - lst.Add(new Duplicati.Library.Interface.CommandLineArgument("server-encryption-key", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Password, Strings.Program.ServerencryptionkeyShort, Strings.Program.ServerencryptionkeyLong(DB_KEY_ENV_NAME, "unencrypted-database"), Library.AutoUpdater.AutoUpdateSettings.AppName + "_Key_42", null, null, "Database encryption is no longer supported, this setting can only be used to decrypt the database")); - return lst.ToArray(); } } @@ -792,7 +796,8 @@ namespace Duplicati.Server string appendfilter = null; string replacefilter = null; - var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(fargs, (key, value) => { + var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(fargs, (key, value) => + { if (key.Equals("source", StringComparison.OrdinalIgnoreCase)) { newsource.Add(value); diff --git a/Duplicati/Server/SingleInstance.cs b/Duplicati/Server/SingleInstance.cs index 156bea027..e29615514 100644 --- a/Duplicati/Server/SingleInstance.cs +++ b/Duplicati/Server/SingleInstance.cs @@ -1,30 +1,28 @@ -// 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. +// 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; using System.Collections.Generic; -using System.Text; using Duplicati.Library.Common; using Duplicati.Library.Common.IO; -using Duplicati.Library.Utility; namespace Duplicati.Server { @@ -138,7 +136,7 @@ namespace Duplicati.Server m_lockfilename = System.IO.Path.Combine(m_controldir, CONTROL_FILE); m_file = null; - + System.IO.Stream temp_fs = null; try @@ -147,7 +145,7 @@ namespace Duplicati.Server temp_fs = PosixFile.OpenExclusive(m_lockfilename, System.IO.FileAccess.Write); else temp_fs = System.IO.File.Open(m_lockfilename, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None); - + if (temp_fs != null) { System.IO.StreamWriter sw = new System.IO.StreamWriter(temp_fs); @@ -161,7 +159,7 @@ namespace Duplicati.Server { if (temp_fs != null) try { temp_fs.Dispose(); } - catch {} + catch { } } //If we have write access @@ -174,7 +172,7 @@ namespace Duplicati.Server DateTime startup = System.IO.File.GetLastWriteTime(m_lockfilename); //Clean up any files that were created before the app launched - foreach(string s in SystemIO.IO_OS.GetFiles(m_controldir)) + foreach (string s in SystemIO.IO_OS.GetFiles(m_controldir)) if (s != m_lockfilename && System.IO.File.GetCreationTime(s) < startup) try { System.IO.File.Delete(s); } catch { } @@ -311,7 +309,7 @@ namespace Duplicati.Server //Finally inform this instance about the call if (SecondInstanceDetected != null) - SecondInstanceDetected(commandline); + SecondInstanceDetected(commandline); } } diff --git a/Duplicati/Server/WebServerLoader.cs b/Duplicati/Server/WebServerLoader.cs new file mode 100644 index 000000000..83b350a5b --- /dev/null +++ b/Duplicati/Server/WebServerLoader.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server; + +/// +/// Helper class for starting the webserver +/// +public static class WebServerLoader +{ + /// + /// The tag used for logging + /// + private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(typeof(WebServerLoader)); + + /// + /// Option for changing the webroot folder + /// + public const string OPTION_WEBROOT = "webservice-webroot"; + + /// + /// Option for changing the webservice listen port + /// + public const string OPTION_PORT = "webservice-port"; + + /// + /// Option for changing the webservice listen interface + /// + public const string OPTION_INTERFACE = "webservice-interface"; + + /// + /// Option for setting the webservice password + /// + public const string OPTION_WEBSERVICE_PASSWORD = "webservice-password"; + + /// + /// Option for setting the webservice allowed hostnames + /// + public const string OPTION_WEBSERVICE_ALLOWEDHOSTNAMES = "webservice-allowedhostnames"; + + /// + /// The default path to the web root + /// + public const string DEFAULT_OPTION_WEBROOT = "webroot"; + + /// + /// The default listening port + /// + public const int DEFAULT_OPTION_PORT = 8200; + + /// + /// Option for setting the webservice SSL certificate + /// + public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile"; + + /// + /// Option for setting the webservice SSL certificate key + /// + public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword"; + + /// + /// The default listening interface + /// + public const string DEFAULT_OPTION_INTERFACE = "loopback"; + + /// + /// The parsed settings for the webserver + /// + /// The root folder with static files + /// The listining port + /// The listening interface + /// The certificate, if any + /// The servername to report + /// The password to access the server + /// The allowed hostnames + public record ParsedWebserverSettings( + string WebRoot, + int Port, + System.Net.IPAddress Interface, + X509Certificate2? Certificate, + string Servername, + string Password, + IEnumerable AllowedHostnames + ); + + + /// + /// Sets up the webserver and starts it + /// + /// A set of options + /// The method to start the server + public static async Task TryRunServer(IReadOnlyDictionary options, Func> createServer) + { + var ports = Enumerable.Empty(); + options.TryGetValue(OPTION_PORT, out var portstring); + if (!string.IsNullOrEmpty(portstring)) + ports = + from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + where int.TryParse(n, out _) + select int.Parse(n); + + if (ports == null || !ports.Any()) + ports = [DEFAULT_OPTION_PORT]; + + options.TryGetValue(OPTION_INTERFACE, out var interfacestring); + + if (string.IsNullOrWhiteSpace(interfacestring)) + interfacestring = FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface; + if (string.IsNullOrWhiteSpace(interfacestring)) + interfacestring = DEFAULT_OPTION_INTERFACE; + + var listenInterface = System.Net.IPAddress.Loopback; + interfacestring = interfacestring.Trim(); + if (new[] { "*", "all", "any" }.Any(x => x.Equals(interfacestring, StringComparison.OrdinalIgnoreCase))) + listenInterface = System.Net.IPAddress.Any; + else if (interfacestring != "loopback") + listenInterface = System.Net.IPAddress.Parse(interfacestring); + + options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out var certificateFile); + options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out var certificateFilePassword); + certificateFilePassword = certificateFilePassword?.Trim() ?? ""; + + X509Certificate2? cert = null; + if (certificateFile == null) + { + try + { + cert = FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate; + } + catch (Exception ex) + { + Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Duplicati.Server.Strings.Server.DefectSSLCertInDatabase); + } + } + else if (certificateFile.Length == 0) + { + FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = null; + } + else + { + try + { + cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable); + } + catch (Exception ex) + { + throw new Exception(Duplicati.Server.Strings.Server.SSLCertificateFailure(ex.Message), ex); + } + } + + var webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR; + +#if DEBUG + //For debug we go "../../../../../.." to get out of "Executables/net8/Duplicati.GUI.TrayIcon/bin/debug/net8.0" + string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", "..", "..", "..")); + tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Duplicati", "Server"); + if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot"))) + webroot = tmpwebroot; +#endif + + webroot = System.IO.Path.Combine(webroot, "webroot"); + + if (options.ContainsKey(OPTION_WEBROOT)) + { + string userroot = options[OPTION_WEBROOT]; +#if DEBUG + //In debug mode we do not care where the path points +#else + //In release mode we check that the user supplied path is located + // in the same folders as the running application, to avoid users + // that inadvertently expose top level folders + if (!string.IsNullOrWhiteSpace(userroot) && userroot.StartsWith(Util.AppendDirSeparator(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison)) + webroot = userroot; +#endif + } + + var certValid = cert != null && cert.HasPrivateKey; + var settings = new ParsedWebserverSettings( + webroot, + -1, + listenInterface, + cert, + string.Format("{0} v{1}", Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version), + options.GetValueOrDefault(OPTION_WEBSERVICE_PASSWORD, ""), + options.GetValueOrDefault(OPTION_WEBSERVICE_ALLOWEDHOSTNAMES, "").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries) + ); + + // If we are in hosted mode with no specified port, + // then try different ports + foreach (var p in ports) + try + { + settings = settings with { Port = p }; + + var server = await createServer(settings); + if (interfacestring != FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface) + FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring; + + if (certValid && cert != FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate) + FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = cert; + + Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Duplicati.Server.Strings.Server.StartedServer(listenInterface.ToString(), p)); + + return server; + } + catch (System.Net.Sockets.SocketException) + { + } + + throw new Exception(Duplicati.Server.Strings.Server.ServerStartFailure(ports)); + } +} \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/CaptchaController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/CaptchaController.js index 2238e66ff..59dc2fe32 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/CaptchaController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/CaptchaController.js @@ -4,7 +4,7 @@ backupApp.controller('CaptchaController', function($scope, CaptchaService, Dialo function refreshImage() { entry.imageurl = null; - AppService.post('/captcha', { 'target': entry.target}).then(function(resp) { + AppService.postJson('/captcha', { 'target': entry.target}).then(function(resp) { entry.token = resp.data.token; entry.imageurl = AppService.apiurl + '/captcha/' + entry.token; diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js index 326ca2e65..63b89844f 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js @@ -69,11 +69,10 @@ backupApp.controller('CommandlineController', function($scope, $routeParams, $lo } options = { - headers: {'Content-Type': 'application/json; charset=utf-8'}, responseType: 'text' }; - AppService.post('/commandline', combined, options).then( + AppService.postJson('/commandline', combined, options).then( function(resp) { $location.path('/commandline/view/' + resp.data.ID); }, @@ -162,7 +161,7 @@ backupApp.controller('CommandlineController', function($scope, $routeParams, $lo } if ($routeParams.backupid != null) { - AppService.get('/backup/' + $routeParams.backupid + '/export?argsonly=true&export-passwords=true').then( + AppService.get('/backup/' + $routeParams.backupid + '/export-argsonly?export-passwords=true').then( function(resp) { $scope.TargetURL = resp.data.Backend; $scope.Arguments = resp.data.Arguments; diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/DeleteController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/DeleteController.js index c7729edb5..c93d7e30a 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/DeleteController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/DeleteController.js @@ -16,7 +16,7 @@ backupApp.controller('DeleteController', function($scope, $routeParams, $locatio $scope.DBPath = $scope.Backup.Backup.DBPath; if ($scope.DBPath != prev || force) - AppService.post('/filesystem/validate', {path: $scope.DBPath}).then(function(resp) { + AppService.postJson('/filesystem/validate', {path: $scope.DBPath}).then(function(resp) { $scope.NoLocalDB = false; }, function() { $scope.NoLocalDB = true; diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/EditBackupController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/EditBackupController.js index 0d14a25f5..696b91eda 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/EditBackupController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/EditBackupController.js @@ -144,7 +144,7 @@ backupApp.controller('EditBackupController', function ($rootScope, $scope, $rout function continuation() { scope.validatingSourcePath = true; - AppService.post('/filesystem/validate', {path: scope.manualSourcePath}).then(function() { + AppService.postJson('/filesystem/validate', {path: scope.manualSourcePath}).then(function() { scope.validatingSourcePath = false; scope.Backup.Sources.push(scope.manualSourcePath); scope.manualSourcePath = null; @@ -439,13 +439,13 @@ backupApp.controller('EditBackupController', function ($rootScope, $scope, $rout if ($routeParams.backupid == null) { function postDb() { - AppService.post('/backups', result, {'headers': {'Content-Type': 'application/json'}}).then(function() { + AppService.postJson('/backups', result).then(function() { $location.path('/'); }, AppUtils.connectionError); }; function checkForExistingDb(continuation) { - AppService.post('/remoteoperation/dbpath', $scope.Backup.TargetURL, {'headers': {'Content-Type': 'application/text'}}).then( + AppService.postJson('/remoteoperation/dbpath', { path: $scope.Backup.TargetURL }).then( function(resp) { if (resp.data.Exists) { DialogService.dialog(gettextCatalog.getString('Use existing database?'), gettextCatalog.getString('An existing local database for the storage has been found.\nRe-using the database will allow the command-line and server instances to work on the same remote storage.\n\n Do you wish to use the existing database?'), [gettextCatalog.getString('Cancel'), gettextCatalog.getString('Yes'), gettextCatalog.getString('No')], function(ix) { @@ -696,7 +696,7 @@ backupApp.controller('EditBackupController', function ($rootScope, $scope, $rout AppService.get('/backupdefaults').then(function(data) { - $scope.rawddata = data.data.data; + $scope.rawddata = data.data; if ($location.$$path.indexOf('/add-import') == 0 && $rootScope.importConfig != null) angular.merge($scope.rawddata, $rootScope.importConfig); @@ -711,7 +711,7 @@ backupApp.controller('EditBackupController', function ($rootScope, $scope, $rout AppService.get('/backup/' + $routeParams.backupid).then(function(data) { - $scope.rawddata = data.data.data; + $scope.rawddata = data.data; setupScope($scope.rawddata); }, function() { diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/ExportController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/ExportController.js index 2e75e8aca..e17649da3 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/ExportController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/ExportController.js @@ -27,7 +27,7 @@ backupApp.controller("ExportController", function($scope, $routeParams, AppServi function getExport() { if ($scope.ExportType === "commandline") { $scope.Connecting = true; - AppService.get("/backup/" + $scope.BackupID + "/export?cmdline=true&export-passwords=" + encodeURIComponent($scope.ExportPasswords)).then( + AppService.get("/backup/" + $scope.BackupID + "/export-cmdline?export-passwords=" + encodeURIComponent($scope.ExportPasswords)).then( function(resp) { $scope.Connecting = false; $scope.Completed = true; diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/ImportController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/ImportController.js index 8f2557785..e9476399f 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/ImportController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/ImportController.js @@ -2,6 +2,7 @@ backupApp.controller('ImportController', function($rootScope, $scope, $timeout, $scope.Connecting = false; $scope.Completed = false; $scope.ImportURL = AppService.get_import_url(); + $scope.XSRFToken = AppService.get_xsrf_token(); $scope.restoremode = $location.$$path.indexOf('/restore-import') == 0; diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/LocalDatabaseController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/LocalDatabaseController.js index 3af527afc..e30e12499 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/LocalDatabaseController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/LocalDatabaseController.js @@ -13,7 +13,7 @@ backupApp.controller('LocalDatabaseController', function($scope, $routeParams, $ $scope.DBPath = $scope.Backup.Backup.DBPath; if ($scope.DBPath != prev || force) - AppService.post('/filesystem/validate', {path: $scope.DBPath}).then(function(resp) { + AppService.postJson('/filesystem/validate', {path: $scope.DBPath}).then(function(resp) { $scope.NoLocalDB = false; }, function() { $scope.NoLocalDB = true; @@ -42,7 +42,7 @@ backupApp.controller('LocalDatabaseController', function($scope, $routeParams, $ }; $scope.doRepair = function() { - AppService.post('/backup/' + $scope.BackupID + '/repair'); + AppService.postJson('/backup/' + $scope.BackupID + '/repair'); $location.path('/'); }; @@ -55,7 +55,7 @@ backupApp.controller('LocalDatabaseController', function($scope, $routeParams, $ $scope.doSave = function(continuation, move) { function doUpdate() { - AppService.post('/backup/' + $scope.BackupID + '/' + (move ? 'movedb' : 'updatedb'), {path: $scope.DBPath}).then( + AppService.postJson('/backup/' + $scope.BackupID + '/' + (move ? 'movedb' : 'updatedb'), {path: $scope.DBPath}).then( function(resp) { $scope.Backup.Backup.DBPath = $scope.DBPath; resetBackupItem(true); @@ -68,7 +68,7 @@ backupApp.controller('LocalDatabaseController', function($scope, $routeParams, $ }; function doCheckTarget() { - AppService.post('/filesystem/validate', {path: $scope.DBPath}).then(function(resp) { + AppService.postJson('/filesystem/validate', {path: $scope.DBPath}).then(function(resp) { DialogService.dialog(gettextCatalog.getString('Existing file found'), gettextCatalog.getString('An existing file was found at the new location\nAre you sure you want the database to point to an existing file?'), [gettextCatalog.getString('Cancel'), gettextCatalog.getString('No'), gettextCatalog.getString('Yes')], function(ix) { if (ix == 2) { doUpdate(); @@ -99,7 +99,7 @@ backupApp.controller('LocalDatabaseController', function($scope, $routeParams, $ }; $scope.doMove = function() { - AppService.post('/filesystem/validate', {path: $scope.DBPath}).then(function(resp) { + AppService.postJson('/filesystem/validate', {path: $scope.DBPath}).then(function(resp) { DialogService.dialog(gettextCatalog.getString('Cannot move to existing file'), gettextCatalog.getString('An existing file was found at the new location')); }, function() { $scope.doSave(null, true); diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js index a837d1acd..d30e0512f 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js @@ -120,7 +120,7 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa $scope.ConnectionProgress = gettextCatalog.getString('Fetching path information …'); inProgress[version] = true; - AppService.post('/backup/' + $scope.BackupID + '/repairupdate', { 'only-paths': true, 'time': filesetStamps[version + '']}).then( + AppService.postJson('/backup/' + $scope.BackupID + '/repairupdate', { 'only-paths': true, 'time': filesetStamps[version + '']}).then( function(resp) { var taskid = resp.data.ID; @@ -159,7 +159,7 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa $scope.ConnectionProgress = gettextCatalog.getString('Fetching path information …'); inProgress[version] = true; - AppService.get('/backup/' + $scope.BackupID + '/files/*?prefix-only=true&folder-contents=false&time=' + encodeURIComponent(stamp)).then( + AppService.get('/backup/' + $scope.BackupID + '/files?prefix-only=true&folder-contents=false&time=' + encodeURIComponent(stamp)).then( function(resp) { delete inProgress[version]; $scope.connecting = false; @@ -219,7 +219,7 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa var version = $scope.RestoreVersion + ''; var stamp = filesetStamps[version]; - AppService.get('/backup/' + $scope.BackupID + '/files/*' + $scope.SearchFilter + '*?prefix-only=false&time=' + encodeURIComponent(stamp) + '&filter=*' + encodeURIComponent($scope.SearchFilter) + '*').then( + AppService.get('/backup/' + $scope.BackupID + '/files?prefix-only=false&time=' + encodeURIComponent(stamp) + '&filter=*' + encodeURIComponent($scope.SearchFilter) + '*').then( function(resp) { $scope.Searching = false; var searchNodes = []; diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreDirectController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreDirectController.js index 8be1ab41d..651af1ac6 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreDirectController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreDirectController.js @@ -58,7 +58,7 @@ backupApp.controller('RestoreDirectController', function ($rootScope, $scope, $l }); } - AppService.post('/backups?temporary=true', obj, {'headers': {'Content-Type': 'application/json'}}).then( + AppService.postJson('/backups?temporary=true', obj).then( function(resp) { $scope.ConnectionProgress = gettextCatalog.getString('Listing backup dates …'); diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js b/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js index f8597cf61..e7dd6e14d 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js @@ -41,7 +41,7 @@ backupApp.directive('backupEditUri', function(gettextCatalog) { dlg.dismiss(); dlg = DialogService.dialog(gettextCatalog.getString('Testing ...'), gettextCatalog.getString('Testing connection ...'), [], null, function() { - AppService.post('/remoteoperation/test', uri).then(function() { + AppService.postJson('/remoteoperation/test', { path: uri }).then(function() { scope.Testing = false; dlg.dismiss(); @@ -60,7 +60,7 @@ backupApp.directive('backupEditUri', function(gettextCatalog) { var createFolder = function() { hasTriedCreate = true; scope.Testing = true; - AppService.post('/remoteoperation/create', uri).then(testConnection, handleError); + AppService.postJson('/remoteoperation/create', { path: uri }).then(testConnection, handleError); }; var appendApprovedCert = function(hash) diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js b/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js index c5a6b026a..4c341c30c 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js @@ -39,7 +39,7 @@ backupApp.directive('restoreFilePicker', function() { // Prefix filter with "@" to prevent Duplicati from // mistaking literal '*' and '?' characters in paths // for glob wildcard characters - AppService.get('/backup/' + $scope.ngBackupId + '/files/' + encodeURIComponent(node.id) + '?prefix-only=false&folder-contents=true&time=' + encodeURIComponent($scope.ngTimestamp) + '&filter=' + encodeURIComponent('@' + node.id)).then(function(data) { + AppService.get('/backup/' + $scope.ngBackupId + '/files?prefix-only=false&folder-contents=true&time=' + encodeURIComponent($scope.ngTimestamp) + '&filter=' + encodeURIComponent('@' + node.id)).then(function(data) { var children = [] for(var n in data.data.Files) diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/sourceFolderPicker.js b/Duplicati/Server/webroot/ngax/scripts/directives/sourceFolderPicker.js index 52d405a84..6baa53f19 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/sourceFolderPicker.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/sourceFolderPicker.js @@ -294,7 +294,7 @@ backupApp.directive('sourceFolderPicker', function() { if (p.substr(0, 1) == '%' && p.substr(p.length - 1, 1) == '%') p += scope.dirsep; - AppService.post('/filesystem/validate', {path: p}).then(function(data) { + AppService.postJson('/filesystem/validate', {path: p}).then(function(data) { defunctmap[compareablePath(data.config.data.path)] = false; }, function(data) { @@ -428,7 +428,7 @@ backupApp.directive('sourceFolderPicker', function() { if (!node.children && !node.loading) { node.loading = true; - AppService.post('/filesystem?onlyfolders=false&showhidden=true', {path: node.id}).then(function(data) { + AppService.postJson('/filesystem?onlyfolders=false&showhidden=true', {path: node.id}).then(function(data) { node.children = data.data; node.loading = false; @@ -520,7 +520,7 @@ backupApp.directive('sourceFolderPicker', function() { // Load filter groups AppUtils.loadFilterGroups(); - AppService.post('/filesystem?onlyfolders=false&showhidden=true', {path: '/'}).then(function(data) { + AppService.postJson('/filesystem?onlyfolders=false&showhidden=true', {path: '/'}).then(function(data) { var usernode = { text: gettextCatalog.getString('User data'), @@ -584,7 +584,7 @@ backupApp.directive('sourceFolderPicker', function() { var node = { leaf: true, id: "%HYPERV%\\" + data.data[i].id, - text: data.data[i].name}; + text: data.data[i].text}; cp = compareablePath(node.id); displayMap[cp] = gettextCatalog.getString('Hyper-V Machine:') + " " + node.text; @@ -616,7 +616,7 @@ backupApp.directive('sourceFolderPicker', function() { var node = { leaf: true, id: "%MSSQL%\\" + data.data[i].id, - text: data.data[i].name + text: data.data[i].text }; cp = compareablePath(node.id); diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/targetFolderPicker.js b/Duplicati/Server/webroot/ngax/scripts/directives/targetFolderPicker.js index bee49af6a..20608905b 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/targetFolderPicker.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/targetFolderPicker.js @@ -63,7 +63,8 @@ backupApp.directive('destinationFolderPicker', function() { if (!node.children && !node.loading) { node.loading = true; - AppService.post('/filesystem?onlyfolders=true&showhidden=true', {path: node.id}).then(function(data) { + AppService.postJson('/filesystem?onlyfolders=true&showhidden=true', {path: node.id}).then(function(data) { + console.log(data); node.children = data.data; node.loading = false; @@ -102,7 +103,7 @@ backupApp.directive('destinationFolderPicker', function() { $scope.$watch('ngHideUserNode', updateHideUserNode); - AppService.post('/filesystem?onlyfolders=true&showhidden=true', {path: '/'}).then(function(data) { + AppService.postJson('/filesystem?onlyfolders=true&showhidden=true', {path: '/'}).then(function(data) { var usernode = { text: 'User data', diff --git a/Duplicati/Server/webroot/ngax/scripts/services/AppService.js b/Duplicati/Server/webroot/ngax/scripts/services/AppService.js index c13043c72..330e2f48f 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/AppService.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/AppService.js @@ -1,6 +1,8 @@ backupApp.service('AppService', function ($http, $cookies, $q, $cookies, DialogService, appConfig) { this.apiurl = '../api/v1'; this.proxy_url = null; + this.xsrftoken = null; + this.xsrfpromise = null; var self = this; @@ -71,38 +73,94 @@ backupApp.service('AppService', function ($http, $cookies, $q, $cookies, DialogS return deferred.promise; }; + var chainPromise = function (deferred, callback) { + callback().then(function (response) { + deferred.resolve(response); + }, function (response) { + deferred.reject(response); + }); + }; + + this.injectXsrf = function (options, callback) { + options.headers = options.headers || {}; + if (this.xsrftoken != null) { + options.headers['X-XSRF-Token'] = this.xsrftoken; + return callback(); + } + + var deferred = $q.defer(); + var self = this; + if (this.xsrfpromise == null) { + this.xsrfpromise = deferred.promise; + var url = this.apiurl + '/antiforgery/token'; + installResponseHook(() => $http.get(this.proxy_url == null ? url : this.proxy_url, setupConfig('GET', {'responseType': 'text'}, null, url))) + .then(function (response) { + self.xsrftoken = response.data; + options.headers['X-XSRF-Token'] = self.xsrftoken; + chainPromise(deferred, callback); + }, function (response) { + deferred.reject(response); + }); + + } else { + this.xsrfpromise.then(() => { + options.headers['X-XSRF-Token'] = self.xsrftoken; + chainPromise(deferred, callback); + }, (response) => deferred.reject(response)); + } + return deferred.promise; + }; + this.get = function (url, options) { let rurl = url; if (!url.startsWith('http')) { rurl = this.apiurl + url; } - return installResponseHook(() => $http.get(this.proxy_url == null ? rurl : this.proxy_url, setupConfig('GET', options, null, rurl))); + options = options || {}; + return this.injectXsrf(options, () => installResponseHook(() => $http.get(this.proxy_url == null ? rurl : this.proxy_url, setupConfig('GET', options, null, rurl)))); }; this.patch = function (url, data, options) { var rurl = this.apiurl + url; - return installResponseHook(() => $http.patch(this.proxy_url == null ? rurl : this.proxy_url, data, setupConfig('PATCH', options, data, rurl))); + options = options || {}; + return this.injectXsrf(options, () => installResponseHook(() => $http.patch(this.proxy_url == null ? rurl : this.proxy_url, data, setupConfig('PATCH', options, data, rurl)))); }; this.post = function (url, data, options) { var rurl = this.apiurl + url; - return installResponseHook(() => $http.post(this.proxy_url == null ? rurl : this.proxy_url, data, setupConfig('POST', options, data, rurl))); + options = options || {}; + return this.injectXsrf(options, () => installResponseHook(() => $http.post(this.proxy_url == null ? rurl : this.proxy_url, data, setupConfig('POST', options, data, rurl)))); + }; + + this.postJson = function (url, data, options) { + + var rurl = this.apiurl + url; + options = options || {}; + options.headers = options.headers || {}; + options.headers['Content-Type'] = 'application/json; charset=utf-8'; + return this.injectXsrf(options, () => installResponseHook(() => $http.post(this.proxy_url == null ? rurl : this.proxy_url, data, setupConfig('POST', options, data, rurl)))); }; this.put = function (url, data, options) { var rurl = this.apiurl + url; - return installResponseHook(() => $http.put(this.proxy_url == null ? rurl : this.proxy_url, data, setupConfig('PUT', options, data, rurl))); + options = options || {}; + return this.injectXsrf(options, () => installResponseHook(() => $http.put(this.proxy_url == null ? rurl : this.proxy_url, data, setupConfig('PUT', options, data, rurl)))); }; this.delete = function (url, options) { var rurl = this.apiurl + url; - return installResponseHook(() => $http.delete(this.proxy_url == null ? rurl : this.proxy_url, setupConfig('DELETE', options, null, rurl))); + options = options || {}; + return this.injectXsrf(options, () => installResponseHook(() => $http.delete(this.proxy_url == null ? rurl : this.proxy_url, setupConfig('DELETE', options, null, rurl)))); }; + this.get_xsrf_token = function () { + return this.xsrftoken; + } + this.get_export_url = function (backupid, passphrase, exportPasswords) { - var rurl = this.apiurl + '/backup/' + backupid + '/export?x-xsrf-token=' + encodeURIComponent($cookies.get('xsrf-token')); - rurl += '&export-passwords=' + encodeURIComponent(exportPasswords); + var rurl = this.apiurl + '/backup/' + backupid + '/export'; + rurl += '?export-passwords=' + encodeURIComponent(exportPasswords); if ((passphrase || '').trim().length > 0) rurl += '&passphrase=' + encodeURIComponent(passphrase); @@ -112,7 +170,7 @@ backupApp.service('AppService', function ($http, $cookies, $q, $cookies, DialogS }; this.get_import_url = function (passphrase) { - var rurl = this.apiurl + '/backups/import?x-xsrf-token=' + encodeURIComponent($cookies.get('xsrf-token')); + var rurl = this.apiurl + '/backups/import'; if ((passphrase || '').trim().length > 0) rurl += '&passphrase=' + encodeURIComponent(passphrase); diff --git a/Duplicati/Server/webroot/ngax/scripts/services/CaptchaService.js b/Duplicati/Server/webroot/ngax/scripts/services/CaptchaService.js index 413b1ae95..1eb9e90a8 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/CaptchaService.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/CaptchaService.js @@ -31,7 +31,7 @@ backupApp.service('CaptchaService', function(DialogService, AppService, AppUtils DialogService.dialog(gettextCatalog.getString('Verifying answer'), gettextCatalog.getString('Verifying ...'), [], function() {}, function() { - AppService.post('/captcha/' + encodeURIComponent(cb.token), {'answer': cb.answer, 'target': cb.target}).then(function(resp) { + AppService.postJson('/captcha/' + encodeURIComponent(cb.token), {'answer': cb.answer, 'target': cb.target}).then(function(resp) { DialogService.dismissCurrent(); self.active = null; cb.callback(cb.token, cb.answer); diff --git a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js index 949036edb..2348851ed 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js @@ -47,7 +47,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, var dlg = null; dlg = DialogService.dialog(gettextCatalog.getString('Testing permissions...'), gettextCatalog.getString('Testing permissions …'), [], null, function () { - AppService.post('/webmodule/s3-iamconfig', { + AppService.postJson('/webmodule/s3-iamconfig', { 's3-operation': 'CanCreateUser', 's3-username': scope.Username, 's3-password': scope.Password @@ -80,7 +80,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, // Loaders are a way for backends to request extra data from the server EditUriBackendConfig.loaders['s3'] = function (scope) { if (scope.s3_providers == null) { - AppService.post('/webmodule/s3-getconfig', {'s3-config': 'Providers'}).then(function (data) { + AppService.postJson('/webmodule/s3-getconfig', {'s3-config': 'Providers'}).then(function (data) { scope.s3_providers = data.data.Result; if (scope.s3_server == undefined && scope.s3_server_custom == undefined) scope.s3_server = 's3.amazonaws.com'; @@ -92,7 +92,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, } if (scope.s3_regions == null) { - AppService.post('/webmodule/s3-getconfig', {'s3-config': 'Regions'}).then(function (data) { + AppService.postJson('/webmodule/s3-getconfig', {'s3-config': 'Regions'}).then(function (data) { scope.s3_regions = data.data.Result; if (scope.s3_region == null && scope.s3_region_custom == undefined) scope.s3_region = ''; @@ -103,7 +103,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, } if (scope.s3_storageclasses == null) { - AppService.post('/webmodule/s3-getconfig', {'s3-config': 'StorageClasses'}).then(function (data) { + AppService.postJson('/webmodule/s3-getconfig', {'s3-config': 'StorageClasses'}).then(function (data) { scope.s3_storageclasses = data.data.Result; if (scope.s3_storageclass == null && scope.s3_storageclass_custom == undefined) scope.s3_storageclass = ''; @@ -123,7 +123,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, dlg = DialogService.dialog(gettextCatalog.getString('Creating user...'), gettextCatalog.getString('Creating new user with limited access …'), [], null, function () { path = (scope.Server || '') + '/' + (scope.Path || ''); - AppService.post('/webmodule/s3-iamconfig', { + AppService.postJson('/webmodule/s3-iamconfig', { 's3-operation': 'CreateIAMUser', 's3-path': path, 's3-username': scope.Username, @@ -147,7 +147,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.validaters['s3'](scope, function () { path = (scope.Server || '') + '/' + (scope.Path || ''); - AppService.post('/webmodule/s3-iamconfig', { + AppService.postJson('/webmodule/s3-iamconfig', { 's3-operation': 'GetPolicyDoc', 's3-path': path }).then(function (data) { @@ -162,7 +162,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.loaders['storj'] = function (scope) { if (scope.storj_satellites == null) { - AppService.post('/webmodule/storj-getconfig', {'storj-config': 'Satellites'}).then(function (data) { + AppService.postJson('/webmodule/storj-getconfig', {'storj-config': 'Satellites'}).then(function (data) { scope.storj_satellites = data.data.Result; if (scope.storj_satellite == undefined && scope.storj_satellite_custom == undefined) scope.storj_satellite = 'us1.storj.io:7777'; @@ -174,7 +174,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, } if (scope.storj_auth_methods == null) { - AppService.post('/webmodule/storj-getconfig', {'storj-config': 'AuthenticationMethods'}).then(function (data) { + AppService.postJson('/webmodule/storj-getconfig', {'storj-config': 'AuthenticationMethods'}).then(function (data) { scope.storj_auth_methods = data.data.Result; if (scope.storj_auth_method == undefined) scope.storj_auth_method = 'API key'; @@ -188,7 +188,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.loaders['tardigrade'] = function (scope) { if (scope.tardigrade_satellites == null) { - AppService.post('/webmodule/storj-getconfig', {'storj-config': 'Satellites'}).then(function (data) { + AppService.postJson('/webmodule/storj-getconfig', {'storj-config': 'Satellites'}).then(function (data) { scope.tardigrade_satellites = data.data.Result; if (scope.tardigrade_satellite == undefined && scope.tardigrade_satellite_custom == undefined) scope.tardigrade_satellite = 'us1.storj.io:7777'; @@ -200,7 +200,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, } if (scope.tardigrade_auth_methods == null) { - AppService.post('/webmodule/storj-getconfig', {'storj-config': 'AuthenticationMethods'}).then(function (data) { + AppService.postJson('/webmodule/storj-getconfig', {'storj-config': 'AuthenticationMethods'}).then(function (data) { scope.tardigrade_auth_methods = data.data.Result; if (scope.tardigrade_auth_method == undefined) scope.tardigrade_auth_method = 'API key'; @@ -306,7 +306,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.loaders['openstack'] = function (scope) { if (scope.openstack_providers == null) { - AppService.post('/webmodule/openstack-getconfig', {'openstack-config': 'Providers'}).then(function (data) { + AppService.postJson('/webmodule/openstack-getconfig', {'openstack-config': 'Providers'}).then(function (data) { scope.openstack_providers = data.data.Result; if (scope.openstack_server == undefined && scope.openstack_server_custom == undefined) scope.openstack_server = 'https://identity.api.rackspacecloud.com/'; @@ -318,7 +318,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, } if (scope.openstack_versions == null) { - AppService.post('/webmodule/openstack-getconfig', {'openstack-config': 'Versions'}).then(function (data) { + AppService.postJson('/webmodule/openstack-getconfig', {'openstack-config': 'Versions'}).then(function (data) { scope.openstack_versions = data.data.Result; if (scope.openstack_version == undefined) scope.openstack_version = 'v2.0'; @@ -333,7 +333,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.loaders['gcs'] = function (scope) { if (scope.gcs_locations == null) { - AppService.post('/webmodule/gcs-getconfig', {'gcs-config': 'Locations'}).then(function (data) { + AppService.postJson('/webmodule/gcs-getconfig', {'gcs-config': 'Locations'}).then(function (data) { scope.gcs_locations = data.data.Result; for (var k in scope.gcs_locations) if (scope.gcs_locations[k] === null) @@ -346,7 +346,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, } if (scope.gcs_storageclasses == null) { - AppService.post('/webmodule/gcs-getconfig', {'gcs-config': 'StorageClasses'}).then(function (data) { + AppService.postJson('/webmodule/gcs-getconfig', {'gcs-config': 'StorageClasses'}).then(function (data) { scope.gcs_storageclasses = data.data.Result; for (var k in scope.gcs_storageclasses) if (scope.gcs_storageclasses[k] === null) diff --git a/Duplicati/Server/webroot/ngax/templates/import.html b/Duplicati/Server/webroot/ngax/templates/import.html index dca98524a..4d40d2888 100644 --- a/Duplicati/Server/webroot/ngax/templates/import.html +++ b/Duplicati/Server/webroot/ngax/templates/import.html @@ -12,7 +12,7 @@
    - +
    @@ -26,6 +26,7 @@
    +
    Import diff --git a/Duplicati/UnitTest/ImportExportTests.cs b/Duplicati/UnitTest/ImportExportTests.cs index 0761b5fa2..590ad88ac 100644 --- a/Duplicati/UnitTest/ImportExportTests.cs +++ b/Duplicati/UnitTest/ImportExportTests.cs @@ -22,12 +22,12 @@ using System; using System.Collections.Generic; using System.IO; +using Duplicati.Library.RestAPI; using Duplicati.Server; using Duplicati.Server.Database; using Duplicati.Server.Serializable; using Duplicati.Server.Serialization; using Duplicati.Server.Serialization.Interface; -using Duplicati.Server.WebServer.RESTMethods; using NUnit.Framework; using Backup = Duplicati.Server.Database.Backup; @@ -66,9 +66,9 @@ namespace Duplicati.UnitTest ID = "1", Metadata = metadata, Name = name, - Settings = new[] {new Setting {Name = "passphrase", Value = "12345"}}, - Sources = new[] {"Mock Backup Source"}, - Tags = new[] {"Tags"}, + Settings = new[] { new Setting { Name = "passphrase", Value = "12345" } }, + Sources = new[] { "Mock Backup Source" }, + Tags = new[] { "Tags" }, TargetURL = $"file:///mock_backup_target?auth-username={username}&auth-password={password}" }; } @@ -79,17 +79,17 @@ namespace Duplicati.UnitTest [TestCase(false)] public void ExportToJSONEncoding(bool removePasswords) { - Dictionary advancedOptions = new Dictionary {{"server-datafolder", this.serverDatafolder}}; + Dictionary advancedOptions = new Dictionary { { "server-datafolder", this.serverDatafolder } }; string usernameKey = "auth-username"; string passwordKey = "auth-password"; string username = @"user%40email.com"; string password = @"abcde12345!@#$%/\"; - IBackup backup = this.CreateBackup("backup", username, password, new Dictionary()); + IBackup backup = this.CreateBackup("backup", username, password, new Dictionary()); if (removePasswords) { - Server.WebServer.RESTMethods.Backup.RemovePasswords(backup); + BackupImportExportHandler.RemovePasswords(backup); } Assert.That(backup.TargetURL, Does.Contain($"{usernameKey}={username}")); @@ -105,9 +105,9 @@ namespace Duplicati.UnitTest } byte[] jsonByteArray; - using (Program.DataConnection = Program.GetDatabaseConnection(null, advancedOptions)) + using (Program.DataConnection = Program.GetDatabaseConnection(advancedOptions)) { - jsonByteArray = Server.WebServer.RESTMethods.Backup.ExportToJSON(backup, null); + jsonByteArray = BackupImportExportHandler.ExportToJSON(backup, null); } // The username should not have the '%40' converted to '@' since the import code @@ -125,41 +125,41 @@ namespace Duplicati.UnitTest [Category("ImportExport")] public void RoundTrip() { - Dictionary metadata = new Dictionary {{"SourceFilesCount", "1"}}; - Dictionary advancedOptions = new Dictionary {{"server-datafolder", this.serverDatafolder}}; - using (Program.DataConnection = Program.GetDatabaseConnection(null, advancedOptions)) + Dictionary metadata = new Dictionary { { "SourceFilesCount", "1" } }; + Dictionary advancedOptions = new Dictionary { { "server-datafolder", this.serverDatafolder } }; + using (Program.DataConnection = Program.GetDatabaseConnection(advancedOptions)) { // Unencrypted file, don't import metadata. string unencryptedWithoutMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName()); - File.WriteAllBytes(unencryptedWithoutMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(this.CreateBackup("unencrypted without metadata", "user", "password", metadata), null)); - Backups.ImportBackup(unencryptedWithoutMetadata, false, () => null, advancedOptions); + File.WriteAllBytes(unencryptedWithoutMetadata, BackupImportExportHandler.ExportToJSON(this.CreateBackup("unencrypted without metadata", "user", "password", metadata), null)); + BackupImportExportHandler.ImportBackup(unencryptedWithoutMetadata, false, () => null, advancedOptions); Assert.AreEqual(1, Program.DataConnection.Backups.Length); Assert.AreEqual(0, Program.DataConnection.Backups[0].Metadata.Count); // Unencrypted file, import metadata. string unencryptedWithMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName()); - File.WriteAllBytes(unencryptedWithMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(this.CreateBackup("unencrypted with metadata", "user", "password", metadata), null)); - Backups.ImportBackup(unencryptedWithMetadata, true, () => null, advancedOptions); + File.WriteAllBytes(unencryptedWithMetadata, BackupImportExportHandler.ExportToJSON(this.CreateBackup("unencrypted with metadata", "user", "password", metadata), null)); + BackupImportExportHandler.ImportBackup(unencryptedWithMetadata, true, () => null, advancedOptions); Assert.AreEqual(2, Program.DataConnection.Backups.Length); Assert.AreEqual(metadata.Count, Program.DataConnection.Backups[1].Metadata.Count); // Encrypted file, don't import metadata. string encryptedWithoutMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName()); string passphrase = "abcde"; - File.WriteAllBytes(encryptedWithoutMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(this.CreateBackup("encrypted without metadata", "user", "password", metadata), passphrase)); - Backups.ImportBackup(encryptedWithoutMetadata, false, () => passphrase, advancedOptions); + File.WriteAllBytes(encryptedWithoutMetadata, BackupImportExportHandler.ExportToJSON(this.CreateBackup("encrypted without metadata", "user", "password", metadata), passphrase)); + BackupImportExportHandler.ImportBackup(encryptedWithoutMetadata, false, () => passphrase, advancedOptions); Assert.AreEqual(3, Program.DataConnection.Backups.Length); Assert.AreEqual(0, Program.DataConnection.Backups[2].Metadata.Count); // Encrypted file, import metadata. string encryptedWithMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName()); - File.WriteAllBytes(encryptedWithMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(this.CreateBackup("encrypted with metadata", "user", "password", metadata), passphrase)); - Backups.ImportBackup(encryptedWithMetadata, true, () => passphrase, advancedOptions); + File.WriteAllBytes(encryptedWithMetadata, BackupImportExportHandler.ExportToJSON(this.CreateBackup("encrypted with metadata", "user", "password", metadata), passphrase)); + BackupImportExportHandler.ImportBackup(encryptedWithMetadata, true, () => passphrase, advancedOptions); Assert.AreEqual(4, Program.DataConnection.Backups.Length); Assert.AreEqual(metadata.Count, Program.DataConnection.Backups[3].Metadata.Count); // Encrypted file, incorrect passphrase. - Assert.Throws(Is.InstanceOf(), () => Backups.ImportBackup(encryptedWithMetadata, true, () => passphrase + " ", advancedOptions)); + Assert.Throws(Is.InstanceOf(), () => BackupImportExportHandler.ImportBackup(encryptedWithMetadata, true, () => passphrase + " ", advancedOptions)); } } } diff --git a/Duplicati/WebserverCore/Abstractions/ICaptchaProvider.cs b/Duplicati/WebserverCore/Abstractions/ICaptchaProvider.cs new file mode 100644 index 000000000..6e417cc4e --- /dev/null +++ b/Duplicati/WebserverCore/Abstractions/ICaptchaProvider.cs @@ -0,0 +1,25 @@ +namespace Duplicati.WebserverCore.Abstractions; + +/// +/// A captcha provider +/// +public interface ICaptchaProvider +{ + /// + /// Check if the captcha is solved + /// + /// The captcha token + /// The captcha target + /// The captcha answer + bool SolvedCaptcha(string token, string target, string answer); + /// + /// Create a captcha + /// + /// The captcha target + string CreateCaptcha(string target); + /// + /// Get the captcha image + /// + /// The captcha token + byte[] GetCaptchaImage(string token); +} diff --git a/Duplicati/WebserverCore/Abstractions/ICommandlineRunService.cs b/Duplicati/WebserverCore/Abstractions/ICommandlineRunService.cs new file mode 100644 index 000000000..50edab625 --- /dev/null +++ b/Duplicati/WebserverCore/Abstractions/ICommandlineRunService.cs @@ -0,0 +1,24 @@ +namespace Duplicati.WebserverCore.Abstractions; + +public interface ICommandlineRunService +{ + public IActiveRun? GetActiveRun(string id); + + string StartTask(string[] args); + + public interface IActiveRun + { + string ID { get; } + DateTime LastAccess { get; } + object Lock { get; } + + bool Finished { get; } + bool Started { get; } + bool IsLogDisposed { get; } + + IEnumerable GetLog(); + + void Abort(); + + } +} diff --git a/Duplicati/WebserverCore/Abstractions/IV1Endpoint.cs b/Duplicati/WebserverCore/Abstractions/IEndpointV1.cs similarity index 79% rename from Duplicati/WebserverCore/Abstractions/IV1Endpoint.cs rename to Duplicati/WebserverCore/Abstractions/IEndpointV1.cs index 057f5d4dc..0b0a961a2 100644 --- a/Duplicati/WebserverCore/Abstractions/IV1Endpoint.cs +++ b/Duplicati/WebserverCore/Abstractions/IEndpointV1.cs @@ -1,6 +1,6 @@ namespace Duplicati.WebserverCore.Abstractions; -public interface IV1Endpoint +public interface IEndpointV1 { public static abstract void Map(RouteGroupBuilder group); } \ No newline at end of file diff --git a/Duplicati/WebserverCore/Abstractions/ILanguageService.cs b/Duplicati/WebserverCore/Abstractions/ILanguageService.cs new file mode 100644 index 000000000..72747dedf --- /dev/null +++ b/Duplicati/WebserverCore/Abstractions/ILanguageService.cs @@ -0,0 +1,6 @@ +namespace Duplicati.WebserverCore.Abstractions; + +public interface ILanguageService +{ + System.Globalization.CultureInfo? GetLanguage(); +} diff --git a/Duplicati/WebserverCore/Abstractions/ISettingsService.cs b/Duplicati/WebserverCore/Abstractions/ISettingsService.cs index 06b07e0a2..b333b1e25 100644 --- a/Duplicati/WebserverCore/Abstractions/ISettingsService.cs +++ b/Duplicati/WebserverCore/Abstractions/ISettingsService.cs @@ -1,3 +1,4 @@ + namespace Duplicati.WebserverCore.Abstractions; public interface ISettingsService diff --git a/Duplicati/WebserverCore/Abstractions/ServerSettings.cs b/Duplicati/WebserverCore/Abstractions/ServerSettings.cs index 647788d98..fb9763c46 100644 --- a/Duplicati/WebserverCore/Abstractions/ServerSettings.cs +++ b/Duplicati/WebserverCore/Abstractions/ServerSettings.cs @@ -1,29 +1,130 @@ + namespace Duplicati.WebserverCore.Abstractions; public class ServerSettings { - public string StartupDelay { get; set; } = ""; - public string DownloadSpeedLimit { get; set; }= ""; - public string UploadSpeedLimit { get; set; }= ""; - public string ThreadPriority { get; set; }= ""; - public string LastWebserverPort { get; set; }= ""; - public string IsFirstRun { get; set; }= ""; - public string ServerPortChanged { get; set; }= ""; - public string ServerPassphrase { get; set; }= ""; - public string ServerPassphraseSalt { get; set; }= ""; - public string ServerPassphraseTrayIcon { get; set; }= ""; - public string ServerPassphraseTrayIconHash { get; set; }= ""; - public string UpdateCheckLast { get; set; }= ""; - public string UpdateCheckInterval { get; set; }= ""; - public string UpdateCheckNewVersion { get; set; }= ""; - public bool UnackedError { get; set; } - public bool UnackedWarning { get; set; } - public string ServerListenInterface { get; set; }= ""; - public string ServerSslCertificate { get; set; }= ""; - public string HasFixedInvalidBackupId { get; set; }= ""; - public string UpdateChannel { get; set; }= ""; - public string UsageReporterLevel { get; set; }= ""; - public string HasAskedForPasswordProtection { get; set; }= ""; - public string DisableTrayIconLogin { get; set; }= ""; - public string ServerAllowedHostnames { get; set; }= ""; + private readonly Server.Database.ServerSettings applicationSettings; + + public ServerSettings(Server.Database.ServerSettings applicationSettings) + => this.applicationSettings = applicationSettings; + + public string StartupDelay + { + get => applicationSettings.StartupDelayDuration; + set => applicationSettings.StartupDelayDuration = value; + } + + public string DownloadSpeedLimit + { + get => applicationSettings.DownloadSpeedLimit; + set => applicationSettings.DownloadSpeedLimit = value; + } + public string UploadSpeedLimit + { + get => applicationSettings.UploadSpeedLimit; + set => applicationSettings.UploadSpeedLimit = value; + } + public ThreadPriority? ThreadPriority + { + get => applicationSettings.ThreadPriorityOverride; + set => applicationSettings.ThreadPriorityOverride = value; + } + public int LastWebserverPort + { + get => applicationSettings.LastWebserverPort; + set => applicationSettings.LastWebserverPort = value; + } + + public bool IsFirstRun + { + get => applicationSettings.IsFirstRun; + set => applicationSettings.IsFirstRun = value; + } + + public bool ServerPortChanged + { + get => applicationSettings.ServerPortChanged; + set => applicationSettings.ServerPortChanged = value; + } + public string ServerPassphrase + { + get => applicationSettings.WebserverPassword; + set => applicationSettings.SetWebserverPassword(value); + } + public string ServerPassphraseSalt + { + get => applicationSettings.WebserverPasswordSalt; + } + public string ServerPassphraseTrayIcon + { + get => applicationSettings.WebserverPasswordTrayIcon; + } + public string ServerPassphraseTrayIconHash + { + get => applicationSettings.WebserverPasswordTrayIconHash; + } + public DateTime UpdateCheckLast + { + get => applicationSettings.LastUpdateCheck; + set => applicationSettings.LastUpdateCheck = value; + } + public string UpdateCheckInterval + { + get => applicationSettings.UpdateCheckInterval; + set => applicationSettings.UpdateCheckInterval = value; + } + public string? UpdateCheckNewVersion + { + get => applicationSettings.UpdatedVersion?.GetUpdateUrls()?.FirstOrDefault(); + } + public bool UnackedError + { + get => applicationSettings.UnackedError; + set => applicationSettings.UnackedError = value; + } + public bool UnackedWarning + { + get => applicationSettings.UnackedWarning; + set => applicationSettings.UnackedWarning = value; + } + public string ServerListenInterface + { + get => applicationSettings.ServerListenInterface; + set => applicationSettings.ServerListenInterface = value; + } + public bool HasFixedInvalidBackupId + { + get => applicationSettings.FixedInvalidBackupId; + set => applicationSettings.FixedInvalidBackupId = value; + } + public string UpdateChannel + { + get => applicationSettings.UpdateChannel; + set => applicationSettings.UpdateChannel = value; + } + public string UsageReporterLevel + { + get => applicationSettings.UsageReporterLevel; + set => applicationSettings.UsageReporterLevel = value; + } + public bool HasAskedForPasswordProtection + { + get => applicationSettings.HasAskedForPasswordProtection; + set => applicationSettings.HasAskedForPasswordProtection = value; + } + public bool DisableTrayIconLogin + { + get => applicationSettings.DisableTrayIconLogin; + set => applicationSettings.DisableTrayIconLogin = value; + } + public string ServerAllowedHostnames + { + get => applicationSettings.AllowedHostnames; + set => applicationSettings.SetAllowedHostnames(value); + } + + public bool HasSSLCertificate + { + get => applicationSettings.ServerSSLCertificate != null; + } } \ No newline at end of file diff --git a/Duplicati/WebserverCore/AuthMiddleware.cs b/Duplicati/WebserverCore/AuthMiddleware.cs deleted file mode 100644 index 67aeb50ba..000000000 --- a/Duplicati/WebserverCore/AuthMiddleware.cs +++ /dev/null @@ -1,35 +0,0 @@ - -namespace Duplicati.WebserverCore -{ - public class AuthMiddleware(RequestDelegate next) - { - private readonly Server.WebServer.AuthenticationHandler _authHandler = new(); - - public async Task InvokeAsync(HttpContext context) - { - var info = new Server.WebServer.RESTMethods.RequestInfo(new LegacyHttpRequestShim(context.Request), new LegacyHttpResponseShim(context.Response), new LegacyHttpSessionShim()); - - if (!_authHandler.Process(info.Request, info.Response, info.Session)) - { - // Call the next delegate/middleware in the pipeline. - await next(context); - } - else - { - if (info.Response is LegacyHttpResponseShim legacy) - { - await legacy.SendAsync(); - } - } - } - } - - public static class AuthMiddlewareExtensions - { - public static IApplicationBuilder UseAuthMiddleware( - this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } -} \ No newline at end of file diff --git a/Duplicati/WebserverCore/Database/Configuration/OptionConfiguration.cs b/Duplicati/WebserverCore/Database/Configuration/OptionConfiguration.cs deleted file mode 100644 index 23a5deb77..000000000 --- a/Duplicati/WebserverCore/Database/Configuration/OptionConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Duplicati.WebserverCore.Database.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Duplicati.WebserverCore.Database.Configuration; - -public class OptionConfiguration : IEntityTypeConfiguration