Added option to disable visual captcha from commandline.

This fixes #5098
This commit is contained in:
Kenneth Skovhede
2024-08-13 13:50:21 +02:00
parent bcd6d43c51
commit 9ecb3a0f17
15 changed files with 9844 additions and 15 deletions
@@ -58,6 +58,7 @@ namespace Duplicati.Server.Database
public const string JWT_CONFIG = "jwt-config";
public const string PBKDF_CONFIG = "pbkdf-config";
public const string AUTOGENERATED_PASSPHRASE = "autogenerated-passphrase";
public const string DISABLE_VISUAL_CAPTCHA = "disable-visual-captcha";
}
private readonly Dictionary<string, string> settings;
@@ -273,6 +274,20 @@ namespace Duplicati.Server.Database
}
}
public bool DisableVisualCaptcha
{
get
{
return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.DISABLE_VISUAL_CAPTCHA], false);
}
set
{
lock (databaseConnection.m_lock)
settings[CONST.DISABLE_VISUAL_CAPTCHA] = value.ToString();
SaveSettings();
}
}
public int LastWebserverPort
{
get
+1
View File
@@ -45,6 +45,7 @@ Error message: {0}", error); }
public static string TempdirLong { get { return LC.L(@"Use this option 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."); } }
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
public static string WebserverResetJwtConfigDescription { get { return LC.L(@"Resets the JWT configuration, invalidating any issued login tokens"); } }
public static string WebserverDisableVisualCaptchaDescription { get { return LC.L(@"Disables the visual captcha"); } }
}
internal static class Scheduler
{
+4
View File
@@ -482,6 +482,9 @@ namespace Duplicati.Server
DataConnection.ExecuteWithCommand((con) => con.ExecuteNonQuery("DELETE FROM TokenFamily"));
}
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA))
DataConnection.ApplicationSettings.DisableVisualCaptcha = Library.Utility.Utility.ParseBool(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA], true);
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_PASSWORD))
DataConnection.ApplicationSettings.SetWebserverPassword(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_PASSWORD]);
else if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(WebServerLoader.OPTION_WEBSERVICE_PASSWORD)))
@@ -800,6 +803,7 @@ namespace Duplicati.Server
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, null, [WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT]),
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_RESET_JWT_CONFIG, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverResetJwtConfigDescription, Strings.Program.WebserverResetJwtConfigDescription),
new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverDisableVisualCaptchaDescription, Strings.Program.WebserverDisableVisualCaptchaDescription),
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)),
+5
View File
@@ -42,6 +42,11 @@ public static class WebServerLoader
/// </summary>
public const string OPTION_WEBSERVICE_RESET_JWT_CONFIG = "webservice-reset-jwt-config";
/// <summary>
/// Option for disabling the visual captcha
/// </summary>
public const string OPTION_WEBSERVICE_DISABLE_VISUAL_CAPTCHA = "webservice-disable-visual-captcha";
/// <summary>
/// Option for setting the webservice allowed hostnames
/// </summary>
@@ -612,6 +612,16 @@ div.captcha {
margin-right: auto;
width: 180px;
}
.code {
background: lightgray;
color: black;
font-family: monospace;
font-size: xx-large;
padding: 10px;
}
.answer {
margin-top: 16px;
}
}
.centered-text {
@@ -1,11 +1,13 @@
backupApp.controller('CaptchaController', function($scope, CaptchaService, DialogService, AppService, AppUtils) {
var entry = $scope.entry = CaptchaService.active;
function refreshImage() {
function refreshChallenge() {
entry.imageurl = null;
AppService.postJson('/captcha', { 'target': entry.target}).then(function(resp) {
entry.token = resp.data.token;
entry.token = resp.data.Token;
entry.expectedAnswer = resp.data.Answer;
entry.noVisualChallenge = resp.data.NoVisualChallenge;
entry.imageurl = AppService.apiurl + '/captcha/' + entry.token;
}, function(err) {
@@ -15,7 +17,7 @@ backupApp.controller('CaptchaController', function($scope, CaptchaService, Dialo
};
if (entry.token == null)
refreshImage();
refreshChallenge();
$scope.reload = refreshImage;
$scope.reload = refreshChallenge;
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,8 +2,9 @@
<div ng-show="entry.imageurl">
<div>{{entry.message}}</div>
<div class="details">
<img src="{{entry.imageurl}}" />
<input type="text" ng-model="entry.answer" />
<img ng-hide="entry.noVisualChallenge" src="{{entry.imageurl}}" />
<code class="code" ng-show="entry.noVisualChallenge">{{entry.expectedAnswer}}</code>
<input class="answer" type="text" ng-model="entry.answer" />
<input type="button" value="{{'Reload' | translate}}" ng-click="reload()" />
</div>
@@ -16,10 +16,15 @@ public interface ICaptchaProvider
/// Create a captcha
/// </summary>
/// <param name="target">The captcha target</param>
string CreateCaptcha(string target);
/// <returns>The captcha token and the answer</returns>
(string Token, string? Answer) CreateCaptcha(string target);
/// <summary>
/// Get the captcha image
/// </summary>
/// <param name="token">The captcha token</param>
byte[] GetCaptchaImage(string token);
/// <summary>
/// Gets a value indicating whether the visual captcha is disabled
/// </summary>
bool VisualCaptchaDisabled { get; }
}
@@ -101,6 +101,11 @@ public class ServerSettings
set => applicationSettings.SetAllowedHostnames(value);
}
public bool DisableVisualCaptcha
{
get => applicationSettings.DisableVisualCaptcha;
}
public bool HasSSLCertificate
{
get => applicationSettings.ServerSSLCertificate != null;
@@ -0,0 +1,2 @@
namespace Duplicati.WebserverCore.Dto;
public sealed record GenerateCaptchaOutput(string Token, string? Answer, bool NoVisualChallenge);
@@ -18,8 +18,12 @@ public class Captcha : IEndpointV1
group.MapPost("/captcha", ([FromServices] ICaptchaProvider captchaProvider, [FromBody] Dto.SolveCaptchaInputDto input) =>
{
var token = captchaProvider.CreateCaptcha(input.target);
return new { token };
var (token, answer) = captchaProvider.CreateCaptcha(input.target);
return new Dto.GenerateCaptchaOutput(
token,
answer,
captchaProvider.VisualCaptchaDisabled
);
})
.RequireAuthorization();
@@ -34,6 +34,7 @@ public class ServerSetting : IEndpointV1
Server.Database.ServerSettings.CONST.SERVER_PASSPHRASE,
Server.Database.ServerSettings.CONST.SERVER_PASSPHRASE_SALT,
Server.Database.ServerSettings.CONST.SERVER_SSL_CERTIFICATE,
Server.Database.ServerSettings.CONST.DISABLE_VISUAL_CAPTCHA,
"ServerSSLCertificate",
"server-passphrase-trayicon-hash",
"server-passphrase-trayicon-salt"
@@ -12,6 +12,12 @@ public class CaptchaService : ICaptchaProvider
{
private readonly object m_lock = new();
private readonly Dictionary<string, CaptchaEntry> m_captchas = [];
private readonly bool m_disableVisualCaptcha;
public CaptchaService(ISettingsService settings)
{
m_disableVisualCaptcha = settings.GetSettings().DisableVisualCaptcha;
}
private class CaptchaEntry
{
@@ -29,7 +35,7 @@ public class CaptchaService : ICaptchaProvider
}
}
public string CreateCaptcha(string target)
public (string Token, string? Answer) CreateCaptcha(string target)
{
var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6);
var nonce = Guid.NewGuid().ToString();
@@ -56,11 +62,14 @@ public class CaptchaService : ICaptchaProvider
m_captchas[token] = new CaptchaEntry(answer, target);
}
return token;
return (token, m_disableVisualCaptcha ? answer : null);
}
public byte[] GetCaptchaImage(string token)
{
if (m_disableVisualCaptcha)
throw new NotFoundException("No such entry");
string? answer = null;
lock (m_lock)
{
@@ -93,6 +102,8 @@ public class CaptchaService : ICaptchaProvider
}
}
public bool VisualCaptchaDisabled => m_disableVisualCaptcha;
public static class CaptchaUtil
{