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 @@
+
+
+
+
-