Merge pull request #3467 from Pectojin/export_passwords
added checkbox to allow exporting backup passwords
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user