diff --git a/Duplicati/Server/Database/Backup.cs b/Duplicati/Server/Database/Backup.cs index c3808c004..5eba71a59 100644 --- a/Duplicati/Server/Database/Backup.cs +++ b/Duplicati/Server/Database/Backup.cs @@ -18,12 +18,28 @@ using System; using Duplicati.Server.Serialization.Interface; using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Linq; namespace Duplicati.Server.Database { public class Backup : IBackup - { - + { + // Sensitive information that may be stored in TargetUrl + private readonly string[] UrlPasswords = new[]{ + "authid", + "auth-password", + "sia-password", + }; + + // Sensitive information that may be stored in Settings + private readonly string[] SettingPasswords = new[]{ + "passphrase", + "--authid", + "--send-mail-password", + "--send-xmpp-password", + }; + public Backup() { this.ID = null; @@ -65,7 +81,7 @@ namespace Duplicati.Server.Database /// public string[] Tags { get; set; } /// - /// The backup target url, excluding username/password + /// The backup target url /// public string TargetURL { get; set; } /// @@ -98,6 +114,27 @@ namespace Duplicati.Server.Database /// public bool IsTemporary { get { return ID == null ? false : ID.IndexOf("-", StringComparison.Ordinal) > 0; } } + /// + /// Sanitizes the backup TargetUrl from any fields in the PasswordFields list. + /// + public void SanitizeTargetUrl() + { + var url = new Duplicati.Library.Utility.Uri(this.TargetURL); + var filteredParameters = url.QueryParameters; + foreach (string field in UrlPasswords) { + filteredParameters.Remove(field); + } + url = url.SetQuery(Duplicati.Library.Utility.Uri.BuildUriQuery(url.QueryParameters)); + this.TargetURL = url.ToString(); + } + + /// + /// Sanitizes the settings from any fields in the PassworldFields list. + /// + public void SanitizeSettings() + { + this.Settings = this.Settings.Where((setting) => !SettingPasswords.Contains(setting.Name)).ToArray(); + } } } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IBackup.cs b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IBackup.cs index e54ed5a9e..9b2f59c76 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Interface/IBackup.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Interface/IBackup.cs @@ -42,7 +42,7 @@ namespace Duplicati.Server.Serialization.Interface /// string[] Tags { get; set; } /// - /// The backup target url, excluding username/password + /// The backup target url /// string TargetURL { get; set; } /// @@ -74,6 +74,10 @@ namespace Duplicati.Server.Serialization.Interface /// Gets a value indicating if this instance is not persisted to the database /// bool IsTemporary { get; } + + void SanitizeTargetUrl(); + + void SanitizeSettings(); } } diff --git a/Duplicati/Server/WebServer/RESTMethods/Backup.cs b/Duplicati/Server/WebServer/RESTMethods/Backup.cs index a7f6d1460..e1057982b 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backup.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Backup.cs @@ -120,6 +120,13 @@ namespace Duplicati.Server.WebServer.RESTMethods { 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.SanitizeSettings(); + backup.SanitizeTargetUrl(); + } + if (cmdline) { info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) }); diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/ExportController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/ExportController.js index 0dd6550a6..a9ab2a428 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/ExportController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/ExportController.js @@ -1,38 +1,67 @@ -backupApp.controller('ExportController', function($scope, $routeParams, AppService, DialogService, gettextCatalog) { - $scope.ExportType = 'file'; +backupApp.controller("ExportController", function($scope, $routeParams, AppService, DialogService, gettextCatalog) { + $scope.ExportType = "file"; + $scope.ExportPasswords = true; $scope.Connecting = false; $scope.BackupID = $routeParams.backupid; $scope.Completed = false; $scope.doExport = function() { - if ($scope.UseEncryption && $scope.ExportType == 'file' && ($scope.Passphrase || '').trim().length == 0) { - DialogService.dialog(gettextCatalog.getString('No passphrase entered'), gettextCatalog.getString('To export without a passphrase, uncheck the "Encrypt file" box')); - return; + // Helper function(s) + function warnUnencryptedPasswords(continuation) { + if ($scope.ExportType === "file" && $scope.ExportPasswords && !$scope.fileEncrypted) { + DialogService.dialog(gettextCatalog.getString("Not using encryption"), gettextCatalog.getString("The configuration should be kept safe. Are you sure you want to save an unencrypted file containing your passwords?"), [gettextCatalog.getString("Cancel"), gettextCatalog.getString("Yes, I understand the risk")], function(ix) { + if (ix === 0) { + $scope.CurrentStep = 0; + } else { + continuation(); + } + }); + } else { + continuation(); + } + } - if ($scope.ExportType == 'commandline') { - $scope.Connecting = true; - AppService.get('/backup/' + $scope.BackupID + '/export?cmdline=true').then( - function(resp) { - $scope.Connecting = false; - $scope.Completed = true; - $scope.CommandLine = resp.data.Command; - }, - function(resp) { - $scope.Connecting = false; - var message = resp.statusText; - if (resp.data != null && resp.data.Message != null) - message = resp.data.Message; + // The actual export function to call after all checks pass + function getExport() { + if ($scope.ExportType === "commandline") { + $scope.Connecting = true; + AppService.get("/backup/" + $scope.BackupID + "/export?cmdline=true&export-passwords=" + encodeURIComponent($scope.ExportPasswords)).then( + function(resp) { + $scope.Connecting = false; + $scope.Completed = true; + $scope.CommandLine = resp.data.Command; + }, + function(resp) { + $scope.Connecting = false; + var message = resp.statusText; + if (resp.data != null && resp.data.Message != null) { + message = resp.data.Message; + } + + DialogService.dialog(gettextCatalog.getString("Error"), gettextCatalog.getString("Failed to connect: {{message}}", { message })); + } + ); + } else { + $scope.DownloadURL = AppService.get_export_url($scope.BackupID, $scope.UseEncryption ? $scope.Passphrase : null, $scope.ExportPasswords); + $scope.Completed = true; + } - DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to connect: {{message}}', { message: message })); - } - ); - } else { - $scope.DownloadURL = AppService.get_export_url($scope.BackupID, $scope.UseEncryption ? $scope.Passphrase : null); - $scope.Completed = true; } + // Make checks that do not require user input + $scope.fileEncrypted = false; + if ($scope.UseEncryption && $scope.ExportType === "file") { + if (($scope.Passphrase || "").trim().length === 0) { + DialogService.dialog(gettextCatalog.getString("No passphrase entered"), gettextCatalog.getString("To export without a passphrase, uncheck the \"Encrypt file\" box")); + return; + } else { + $scope.fileEncrypted = true; + } + } + + // Chain various checks that require user input + warnUnencryptedPasswords(function() { getExport(); }); }; - }); diff --git a/Duplicati/Server/webroot/ngax/scripts/services/AppService.js b/Duplicati/Server/webroot/ngax/scripts/services/AppService.js index 54ffbd112..d03654d11 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/AppService.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/AppService.js @@ -35,19 +35,19 @@ backupApp.service('AppService', function($http, $cookies, $q, $cookies, DialogSe if (($cookies.get('ui-locale') || '').trim().length > 0) options.headers['X-UI-Language'] = $cookies.get('ui-locale'); - + if (self.proxy_config != null) self.proxy_config(method, options, data, targeturl); return options; }; - - var installResponseHook = function(promise) { + + var installResponseHook = function(promise) { var deferred = $q.defer(); - + promise.then(function successCallback(response) { deferred.resolve(response); - }, function errorCallback(response) { + }, function errorCallback(response) { if (response.status == 401){ DialogService.dismissAll(); DialogService.accept('Not logged in', function () { @@ -55,18 +55,18 @@ backupApp.service('AppService', function($http, $cookies, $q, $cookies, DialogSe }); return; } - deferred.reject(response); + deferred.reject(response); }); - + return deferred.promise; }; - this.get = function(url, options) { + this.get = function(url, options) { var rurl = this.apiurl + url; - + return 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))); @@ -88,8 +88,9 @@ backupApp.service('AppService', function($http, $cookies, $q, $cookies, DialogSe }; - this.get_export_url = function(backupid, passphrase) { + 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); if ((passphrase || '').trim().length > 0) rurl += '&passphrase=' + encodeURIComponent(passphrase); diff --git a/Duplicati/Server/webroot/ngax/templates/export.html b/Duplicati/Server/webroot/ngax/templates/export.html index d9caf839b..b82bbd807 100644 --- a/Duplicati/Server/webroot/ngax/templates/export.html +++ b/Duplicati/Server/webroot/ngax/templates/export.html @@ -13,11 +13,15 @@ +
+ + +
-
+