Merge pull request #3467 from Pectojin/export_passwords

added checkbox to allow exporting backup passwords
This commit is contained in:
Kenneth Skovhede
2018-11-12 13:23:34 +01:00
committed by GitHub
6 changed files with 123 additions and 41 deletions
+40 -3
View File
@@ -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
/// </summary>
public string[] Tags { get; set; }
/// <summary>
/// The backup target url, excluding username/password
/// The backup target url
/// </summary>
public string TargetURL { get; set; }
/// <summary>
@@ -98,6 +114,27 @@ namespace Duplicati.Server.Database
/// </summary>
public bool IsTemporary { get { return ID == null ? false : ID.IndexOf("-", StringComparison.Ordinal) > 0; } }
/// <summary>
/// Sanitizes the backup TargetUrl from any fields in the PasswordFields list.
/// </summary>
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();
}
/// <summary>
/// Sanitizes the settings from any fields in the PassworldFields list.
/// </summary>
public void SanitizeSettings()
{
this.Settings = this.Settings.Where((setting) => !SettingPasswords.Contains(setting.Name)).ToArray();
}
}
}
@@ -42,7 +42,7 @@ namespace Duplicati.Server.Serialization.Interface
/// </summary>
string[] Tags { get; set; }
/// <summary>
/// The backup target url, excluding username/password
/// The backup target url
/// </summary>
string TargetURL { get; set; }
/// <summary>
@@ -74,6 +74,10 @@ namespace Duplicati.Server.Serialization.Interface
/// Gets a value indicating if this instance is not persisted to the database
/// </summary>
bool IsTemporary { get; }
void SanitizeTargetUrl();
void SanitizeSettings();
}
}
@@ -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)) });
@@ -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(); });
};
});
@@ -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);
@@ -13,11 +13,15 @@
<input type="radio" name="export-type" value="file" id="type-file" ng-model="ExportType"/>
<label for="type-file" translate>To File</label>
</div>
<div class="input checkbox multiple">
<input type="checkbox" name="encrypt-passwords" id="encrypt-passwords" ng-model="ExportPasswords" />
<label for="encrypt-passwords" translate>Export passwords</label>
</div>
<div class="input checkbox multiple" ng-show="ExportType == 'file'">
<input type="checkbox" name="use-encryption" id="use-encryption" ng-model="UseEncryption" />
<label for="use-encryption" translate>Encrypt file</label>
</div>
<div class="input text" ng-show="UseEncryption">
<div class="input text" ng-show="UseEncryption">
<label for="encryption-passphrase" translate>Passphrase</label>
<input type="password" name="encryption-password" id="encryption-passphrase" ng-model="Passphrase" placeholder="{{'Enter encryption passphrase' | translate}}" >
</div>