Drop WebUI default credentials

PR #19777.
This commit is contained in:
Vladimir Golovnev 2023-11-10 07:18:42 +03:00 committed by Vladimir Golovnev (Glassez)
parent 2c2252d7d9
commit 786c09e981
No known key found for this signature in database
GPG key ID: 52A2C7DEE2DFA6F7
21 changed files with 378 additions and 254 deletions

View file

@ -96,7 +96,6 @@
#include "gui/mainwindow.h"
#include "gui/shutdownconfirmdialog.h"
#include "gui/uithememanager.h"
#include "gui/utils.h"
#include "gui/windowstate.h"
#ifdef Q_OS_WIN
@ -106,6 +105,9 @@
#ifndef DISABLE_WEBUI
#include "webui/webui.h"
#ifdef DISABLE_GUI
#include "base/utils/password.h"
#endif
#endif
namespace
@ -310,8 +312,8 @@ Application::Application(int &argc, char **argv)
if (isFileLoggerEnabled())
m_fileLogger = new FileLogger(fileLoggerPath(), isFileLoggerBackup(), fileLoggerMaxSize(), isFileLoggerDeleteOld(), fileLoggerAge(), static_cast<FileLogger::FileLogAgeType>(fileLoggerAgeType()));
if (m_commandLineArgs.webUiPort > 0) // it will be -1 when user did not set any value
Preferences::instance()->setWebUiPort(m_commandLineArgs.webUiPort);
if (m_commandLineArgs.webUIPort > 0) // it will be -1 when user did not set any value
Preferences::instance()->setWebUIPort(m_commandLineArgs.webUIPort);
if (m_commandLineArgs.torrentingPort > 0) // it will be -1 when user did not set any value
{
@ -899,25 +901,28 @@ int Application::exec()
#endif // DISABLE_GUI
#ifndef DISABLE_WEBUI
#ifndef DISABLE_GUI
m_webui = new WebUI(this);
#ifdef DISABLE_GUI
#else
const Preferences *pref = Preferences::instance();
const QString tempPassword = pref->getWebUIPassword().isEmpty()
? Utils::Password::generate() : QString();
m_webui = new WebUI(this, (!tempPassword.isEmpty() ? Utils::Password::PBKDF2::generate(tempPassword) : QByteArray()));
if (m_webui->isErrored())
QCoreApplication::exit(EXIT_FAILURE);
connect(m_webui, &WebUI::fatalError, this, []() { QCoreApplication::exit(EXIT_FAILURE); });
const Preferences *pref = Preferences::instance();
const auto scheme = pref->isWebUiHttpsEnabled() ? u"https"_s : u"http"_s;
const auto url = u"%1://localhost:%2\n"_s.arg(scheme, QString::number(pref->getWebUiPort()));
const auto scheme = pref->isWebUIHttpsEnabled() ? u"https"_s : u"http"_s;
const auto url = u"%1://localhost:%2\n"_s.arg(scheme, QString::number(pref->getWebUIPort()));
const QString mesg = u"\n******** %1 ********\n"_s.arg(tr("Information"))
+ tr("To control qBittorrent, access the WebUI at: %1").arg(url);
printf("%s\n", qUtf8Printable(mesg));
if (pref->getWebUIPassword() == QByteArrayLiteral("ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ=="))
if (!tempPassword.isEmpty())
{
const QString warning = tr("The Web UI administrator username is: %1").arg(pref->getWebUiUsername()) + u'\n'
+ tr("The Web UI administrator password has not been changed from the default: %1").arg(u"adminadmin"_s) + u'\n'
+ tr("This is a security risk, please change your password in program preferences.") + u'\n';
const QString warning = tr("The WebUI administrator username is: %1").arg(pref->getWebUIUsername()) + u'\n'
+ tr("The WebUI administrator password was not set. A temporary password is provided for this session: %1").arg(tempPassword) + u'\n'
+ tr("You should set your own password in program preferences.") + u'\n';
printf("%s", qUtf8Printable(warning));
}
#endif // DISABLE_GUI
@ -1300,3 +1305,10 @@ void Application::cleanup()
Utils::Misc::shutdownComputer(m_shutdownAct);
}
}
#ifndef DISABLE_WEBUI
WebUI *Application::webUI() const
{
return m_webui;
}
#endif

View file

@ -149,6 +149,10 @@ private slots:
#endif
private:
#ifndef DISABLE_WEBUI
WebUI *webUI() const override;
#endif
void initializeTranslation();
void processParams(const QBtCommandLineParameters &params);
void runExternalProgram(const QString &programTemplate, const BitTorrent::Torrent *torrent) const;

View file

@ -349,7 +349,7 @@ QBtCommandLineParameters::QBtCommandLineParameters(const QProcessEnvironment &en
#elif !defined(Q_OS_WIN)
, shouldDaemonize(DAEMON_OPTION.value(env))
#endif
, webUiPort(WEBUI_PORT_OPTION.value(env, -1))
, webUIPort(WEBUI_PORT_OPTION.value(env, -1))
, torrentingPort(TORRENTING_PORT_OPTION.value(env, -1))
, skipDialog(SKIP_DIALOG_OPTION.value(env))
, profileDir(PROFILE_OPTION.value(env))
@ -387,8 +387,8 @@ QBtCommandLineParameters parseCommandLine(const QStringList &args)
#endif
else if (arg == WEBUI_PORT_OPTION)
{
result.webUiPort = WEBUI_PORT_OPTION.value(arg);
if ((result.webUiPort < 1) || (result.webUiPort > 65535))
result.webUIPort = WEBUI_PORT_OPTION.value(arg);
if ((result.webUIPort < 1) || (result.webUIPort > 65535))
throw CommandLineParameterError(QCoreApplication::translate("CMD Options", "%1 must specify a valid port (1 to 65535).")
.arg(u"--webui-port"_s));
}
@ -509,7 +509,7 @@ QString makeUsage(const QString &prgName)
#endif
+ SHOW_HELP_OPTION.usage() + wrapText(QCoreApplication::translate("CMD Options", "Display this help message and exit")) + u'\n'
+ WEBUI_PORT_OPTION.usage(QCoreApplication::translate("CMD Options", "port"))
+ wrapText(QCoreApplication::translate("CMD Options", "Change the Web UI port"))
+ wrapText(QCoreApplication::translate("CMD Options", "Change the WebUI port"))
+ u'\n'
+ TORRENTING_PORT_OPTION.usage(QCoreApplication::translate("CMD Options", "port"))
+ wrapText(QCoreApplication::translate("CMD Options", "Change the torrenting port"))

View file

@ -53,7 +53,7 @@ struct QBtCommandLineParameters
#elif !defined(Q_OS_WIN)
bool shouldDaemonize = false;
#endif
int webUiPort = -1;
int webUIPort = -1;
int torrentingPort = -1;
std::optional<bool> skipDialog;
Path profileDir;

View file

@ -36,6 +36,7 @@
class QString;
class Path;
class WebUI;
struct QBtCommandLineParameters;
#ifdef Q_OS_WIN
@ -83,4 +84,8 @@ public:
virtual MemoryPriority processMemoryPriority() const = 0;
virtual void setProcessMemoryPriority(MemoryPriority priority) = 0;
#endif
#ifndef DISABLE_WEBUI
virtual WebUI *webUI() const = 0;
#endif
};

View file

@ -628,7 +628,7 @@ void Preferences::setSearchEnabled(const bool enabled)
setValue(u"Preferences/Search/SearchEnabled"_s, enabled);
}
bool Preferences::isWebUiEnabled() const
bool Preferences::isWebUIEnabled() const
{
#ifdef DISABLE_GUI
const bool defaultValue = true;
@ -638,41 +638,41 @@ bool Preferences::isWebUiEnabled() const
return value(u"Preferences/WebUI/Enabled"_s, defaultValue);
}
void Preferences::setWebUiEnabled(const bool enabled)
void Preferences::setWebUIEnabled(const bool enabled)
{
if (enabled == isWebUiEnabled())
if (enabled == isWebUIEnabled())
return;
setValue(u"Preferences/WebUI/Enabled"_s, enabled);
}
bool Preferences::isWebUiLocalAuthEnabled() const
bool Preferences::isWebUILocalAuthEnabled() const
{
return value(u"Preferences/WebUI/LocalHostAuth"_s, true);
}
void Preferences::setWebUiLocalAuthEnabled(const bool enabled)
void Preferences::setWebUILocalAuthEnabled(const bool enabled)
{
if (enabled == isWebUiLocalAuthEnabled())
if (enabled == isWebUILocalAuthEnabled())
return;
setValue(u"Preferences/WebUI/LocalHostAuth"_s, enabled);
}
bool Preferences::isWebUiAuthSubnetWhitelistEnabled() const
bool Preferences::isWebUIAuthSubnetWhitelistEnabled() const
{
return value(u"Preferences/WebUI/AuthSubnetWhitelistEnabled"_s, false);
}
void Preferences::setWebUiAuthSubnetWhitelistEnabled(const bool enabled)
void Preferences::setWebUIAuthSubnetWhitelistEnabled(const bool enabled)
{
if (enabled == isWebUiAuthSubnetWhitelistEnabled())
if (enabled == isWebUIAuthSubnetWhitelistEnabled())
return;
setValue(u"Preferences/WebUI/AuthSubnetWhitelistEnabled"_s, enabled);
}
QVector<Utils::Net::Subnet> Preferences::getWebUiAuthSubnetWhitelist() const
QVector<Utils::Net::Subnet> Preferences::getWebUIAuthSubnetWhitelist() const
{
const auto subnets = value<QStringList>(u"Preferences/WebUI/AuthSubnetWhitelist"_s);
@ -689,7 +689,7 @@ QVector<Utils::Net::Subnet> Preferences::getWebUiAuthSubnetWhitelist() const
return ret;
}
void Preferences::setWebUiAuthSubnetWhitelist(QStringList subnets)
void Preferences::setWebUIAuthSubnetWhitelist(QStringList subnets)
{
Algorithm::removeIf(subnets, [](const QString &subnet)
{
@ -712,27 +712,27 @@ void Preferences::setServerDomains(const QString &str)
setValue(u"Preferences/WebUI/ServerDomains"_s, str);
}
QString Preferences::getWebUiAddress() const
QString Preferences::getWebUIAddress() const
{
return value<QString>(u"Preferences/WebUI/Address"_s, u"*"_s).trimmed();
}
void Preferences::setWebUiAddress(const QString &addr)
void Preferences::setWebUIAddress(const QString &addr)
{
if (addr == getWebUiAddress())
if (addr == getWebUIAddress())
return;
setValue(u"Preferences/WebUI/Address"_s, addr.trimmed());
}
quint16 Preferences::getWebUiPort() const
quint16 Preferences::getWebUIPort() const
{
return value<quint16>(u"Preferences/WebUI/Port"_s, 8080);
}
void Preferences::setWebUiPort(const quint16 port)
void Preferences::setWebUIPort(const quint16 port)
{
if (port == getWebUiPort())
if (port == getWebUIPort())
return;
// cast to `int` type so it will show human readable unit in configuration file
@ -752,14 +752,14 @@ void Preferences::setUPnPForWebUIPort(const bool enabled)
setValue(u"Preferences/WebUI/UseUPnP"_s, enabled);
}
QString Preferences::getWebUiUsername() const
QString Preferences::getWebUIUsername() const
{
return value<QString>(u"Preferences/WebUI/Username"_s, u"admin"_s);
}
void Preferences::setWebUiUsername(const QString &username)
void Preferences::setWebUIUsername(const QString &username)
{
if (username == getWebUiUsername())
if (username == getWebUIUsername())
return;
setValue(u"Preferences/WebUI/Username"_s, username);
@ -767,9 +767,7 @@ void Preferences::setWebUiUsername(const QString &username)
QByteArray Preferences::getWebUIPassword() const
{
// default: adminadmin
const auto defaultValue = QByteArrayLiteral("ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ==");
return value(u"Preferences/WebUI/Password_PBKDF2"_s, defaultValue);
return value<QByteArray>(u"Preferences/WebUI/Password_PBKDF2"_s);
}
void Preferences::setWebUIPassword(const QByteArray &password)
@ -832,40 +830,40 @@ void Preferences::setWebAPISessionCookieName(const QString &cookieName)
setValue(u"WebAPI/SessionCookieName"_s, cookieName);
}
bool Preferences::isWebUiClickjackingProtectionEnabled() const
bool Preferences::isWebUIClickjackingProtectionEnabled() const
{
return value(u"Preferences/WebUI/ClickjackingProtection"_s, true);
}
void Preferences::setWebUiClickjackingProtectionEnabled(const bool enabled)
void Preferences::setWebUIClickjackingProtectionEnabled(const bool enabled)
{
if (enabled == isWebUiClickjackingProtectionEnabled())
if (enabled == isWebUIClickjackingProtectionEnabled())
return;
setValue(u"Preferences/WebUI/ClickjackingProtection"_s, enabled);
}
bool Preferences::isWebUiCSRFProtectionEnabled() const
bool Preferences::isWebUICSRFProtectionEnabled() const
{
return value(u"Preferences/WebUI/CSRFProtection"_s, true);
}
void Preferences::setWebUiCSRFProtectionEnabled(const bool enabled)
void Preferences::setWebUICSRFProtectionEnabled(const bool enabled)
{
if (enabled == isWebUiCSRFProtectionEnabled())
if (enabled == isWebUICSRFProtectionEnabled())
return;
setValue(u"Preferences/WebUI/CSRFProtection"_s, enabled);
}
bool Preferences::isWebUiSecureCookieEnabled() const
bool Preferences::isWebUISecureCookieEnabled() const
{
return value(u"Preferences/WebUI/SecureCookie"_s, true);
}
void Preferences::setWebUiSecureCookieEnabled(const bool enabled)
void Preferences::setWebUISecureCookieEnabled(const bool enabled)
{
if (enabled == isWebUiSecureCookieEnabled())
if (enabled == isWebUISecureCookieEnabled())
return;
setValue(u"Preferences/WebUI/SecureCookie"_s, enabled);
@ -884,14 +882,14 @@ void Preferences::setWebUIHostHeaderValidationEnabled(const bool enabled)
setValue(u"Preferences/WebUI/HostHeaderValidation"_s, enabled);
}
bool Preferences::isWebUiHttpsEnabled() const
bool Preferences::isWebUIHttpsEnabled() const
{
return value(u"Preferences/WebUI/HTTPS/Enabled"_s, false);
}
void Preferences::setWebUiHttpsEnabled(const bool enabled)
void Preferences::setWebUIHttpsEnabled(const bool enabled)
{
if (enabled == isWebUiHttpsEnabled())
if (enabled == isWebUIHttpsEnabled())
return;
setValue(u"Preferences/WebUI/HTTPS/Enabled"_s, enabled);
@ -923,27 +921,27 @@ void Preferences::setWebUIHttpsKeyPath(const Path &path)
setValue(u"Preferences/WebUI/HTTPS/KeyPath"_s, path);
}
bool Preferences::isAltWebUiEnabled() const
bool Preferences::isAltWebUIEnabled() const
{
return value(u"Preferences/WebUI/AlternativeUIEnabled"_s, false);
}
void Preferences::setAltWebUiEnabled(const bool enabled)
void Preferences::setAltWebUIEnabled(const bool enabled)
{
if (enabled == isAltWebUiEnabled())
if (enabled == isAltWebUIEnabled())
return;
setValue(u"Preferences/WebUI/AlternativeUIEnabled"_s, enabled);
}
Path Preferences::getWebUiRootFolder() const
Path Preferences::getWebUIRootFolder() const
{
return value<Path>(u"Preferences/WebUI/RootFolder"_s);
}
void Preferences::setWebUiRootFolder(const Path &path)
void Preferences::setWebUIRootFolder(const Path &path)
{
if (path == getWebUiRootFolder())
if (path == getWebUIRootFolder())
return;
setValue(u"Preferences/WebUI/RootFolder"_s, path);

View file

@ -169,26 +169,26 @@ public:
void setSearchEnabled(bool enabled);
// HTTP Server
bool isWebUiEnabled() const;
void setWebUiEnabled(bool enabled);
bool isWebUIEnabled() const;
void setWebUIEnabled(bool enabled);
QString getServerDomains() const;
void setServerDomains(const QString &str);
QString getWebUiAddress() const;
void setWebUiAddress(const QString &addr);
quint16 getWebUiPort() const;
void setWebUiPort(quint16 port);
QString getWebUIAddress() const;
void setWebUIAddress(const QString &addr);
quint16 getWebUIPort() const;
void setWebUIPort(quint16 port);
bool useUPnPForWebUIPort() const;
void setUPnPForWebUIPort(bool enabled);
// Authentication
bool isWebUiLocalAuthEnabled() const;
void setWebUiLocalAuthEnabled(bool enabled);
bool isWebUiAuthSubnetWhitelistEnabled() const;
void setWebUiAuthSubnetWhitelistEnabled(bool enabled);
QVector<Utils::Net::Subnet> getWebUiAuthSubnetWhitelist() const;
void setWebUiAuthSubnetWhitelist(QStringList subnets);
QString getWebUiUsername() const;
void setWebUiUsername(const QString &username);
bool isWebUILocalAuthEnabled() const;
void setWebUILocalAuthEnabled(bool enabled);
bool isWebUIAuthSubnetWhitelistEnabled() const;
void setWebUIAuthSubnetWhitelistEnabled(bool enabled);
QVector<Utils::Net::Subnet> getWebUIAuthSubnetWhitelist() const;
void setWebUIAuthSubnetWhitelist(QStringList subnets);
QString getWebUIUsername() const;
void setWebUIUsername(const QString &username);
QByteArray getWebUIPassword() const;
void setWebUIPassword(const QByteArray &password);
int getWebUIMaxAuthFailCount() const;
@ -201,26 +201,26 @@ public:
void setWebAPISessionCookieName(const QString &cookieName);
// WebUI security
bool isWebUiClickjackingProtectionEnabled() const;
void setWebUiClickjackingProtectionEnabled(bool enabled);
bool isWebUiCSRFProtectionEnabled() const;
void setWebUiCSRFProtectionEnabled(bool enabled);
bool isWebUiSecureCookieEnabled () const;
void setWebUiSecureCookieEnabled(bool enabled);
bool isWebUIClickjackingProtectionEnabled() const;
void setWebUIClickjackingProtectionEnabled(bool enabled);
bool isWebUICSRFProtectionEnabled() const;
void setWebUICSRFProtectionEnabled(bool enabled);
bool isWebUISecureCookieEnabled () const;
void setWebUISecureCookieEnabled(bool enabled);
bool isWebUIHostHeaderValidationEnabled() const;
void setWebUIHostHeaderValidationEnabled(bool enabled);
// HTTPS
bool isWebUiHttpsEnabled() const;
void setWebUiHttpsEnabled(bool enabled);
bool isWebUIHttpsEnabled() const;
void setWebUIHttpsEnabled(bool enabled);
Path getWebUIHttpsCertificatePath() const;
void setWebUIHttpsCertificatePath(const Path &path);
Path getWebUIHttpsKeyPath() const;
void setWebUIHttpsKeyPath(const Path &path);
bool isAltWebUiEnabled() const;
void setAltWebUiEnabled(bool enabled);
Path getWebUiRootFolder() const;
void setWebUiRootFolder(const Path &path);
bool isAltWebUIEnabled() const;
void setAltWebUIEnabled(bool enabled);
Path getWebUIRootFolder() const;
void setWebUIRootFolder(const Path &path);
// WebUI custom HTTP headers
bool isWebUICustomHTTPHeadersEnabled() const;

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Mike Tzou (Chocobo1)
*
* This program is free software; you can redistribute it and/or
@ -36,6 +37,7 @@
#include <QString>
#include <QVector>
#include "base/global.h"
#include "bytearray.h"
#include "random.h"
@ -65,6 +67,21 @@ bool Utils::Password::slowEquals(const QByteArray &a, const QByteArray &b)
return (diff == 0);
}
QString Utils::Password::generate()
{
const QString alphanum = u"23456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"_s;
const int passwordLength = 9;
QString pass;
pass.reserve(passwordLength);
while (pass.length() < passwordLength)
{
const auto num = Utils::Random::rand(0, (alphanum.size() - 1));
pass.append(alphanum[num]);
}
return pass;
}
QByteArray Utils::Password::PBKDF2::generate(const QString &password)
{
return generate(password.toUtf8());
@ -72,9 +89,8 @@ QByteArray Utils::Password::PBKDF2::generate(const QString &password)
QByteArray Utils::Password::PBKDF2::generate(const QByteArray &password)
{
const std::array<uint32_t, 4> salt
{{Random::rand(), Random::rand()
, Random::rand(), Random::rand()}};
const std::array<uint32_t, 4> salt {
{Random::rand(), Random::rand(), Random::rand(), Random::rand()}};
std::array<unsigned char, 64> outBuf {};
const int hmacResult = PKCS5_PBKDF2_HMAC(password.constData(), password.size()

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Mike Tzou (Chocobo1)
*
* This program is free software; you can redistribute it and/or
@ -37,6 +38,8 @@ namespace Utils::Password
// Taken from https://crackstation.net/hashing-security.htm
bool slowEquals(const QByteArray &a, const QByteArray &b);
QString generate();
namespace PBKDF2
{
QByteArray generate(const QString &password);

View file

@ -50,7 +50,7 @@ IPSubnetWhitelistOptionsDialog::IPSubnetWhitelistOptionsDialog(QWidget *parent)
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
QStringList authSubnetWhitelistStringList;
for (const Utils::Net::Subnet &subnet : asConst(Preferences::instance()->getWebUiAuthSubnetWhitelist()))
for (const Utils::Net::Subnet &subnet : asConst(Preferences::instance()->getWebUIAuthSubnetWhitelist()))
authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet);
m_model = new QStringListModel(authSubnetWhitelistStringList, this);
@ -81,7 +81,7 @@ void IPSubnetWhitelistOptionsDialog::on_buttonBox_accepted()
// Operate on the m_sortFilter to grab the strings in sorted order
for (int i = 0; i < m_sortFilter->rowCount(); ++i)
subnets.append(m_sortFilter->index(i, 0).data().toString());
Preferences::instance()->setWebUiAuthSubnetWhitelist(subnets);
Preferences::instance()->setWebUIAuthSubnetWhitelist(subnets);
QDialog::accept();
}
else

View file

@ -1182,7 +1182,7 @@ void MainWindow::closeEvent(QCloseEvent *e)
if (!isVisible())
show();
QMessageBox confirmBox(QMessageBox::Question, tr("Exiting qBittorrent"),
// Split it because the last sentence is used in the Web UI
// Split it because the last sentence is used in the WebUI
tr("Some files are currently transferring.") + u'\n' + tr("Are you sure you want to quit qBittorrent?"),
QMessageBox::NoButton, this);
QPushButton *noBtn = confirmBox.addButton(tr("&No"), QMessageBox::NoRole);

View file

@ -69,6 +69,7 @@
#include "utils.h"
#include "watchedfolderoptionsdialog.h"
#include "watchedfoldersmodel.h"
#include "webui/webui.h"
#ifndef DISABLE_WEBUI
#include "base/net/dnsupdater.h"
@ -80,6 +81,9 @@
#define SETTINGS_KEY(name) u"OptionsDialog/" name
const int WEBUI_MIN_USERNAME_LENGTH = 3;
const int WEBUI_MIN_PASSWORD_LENGTH = 6;
namespace
{
QStringList translatedWeekdayNames()
@ -106,6 +110,16 @@ namespace
}
};
bool isValidWebUIUsername(const QString &username)
{
return (username.length() >= WEBUI_MIN_USERNAME_LENGTH);
}
bool isValidWebUIPassword(const QString &password)
{
return (password.length() >= WEBUI_MIN_PASSWORD_LENGTH);
}
// Shortcuts for frequently used signals that have more than one overload. They would require
// type casts and that is why we declare required member pointer here instead.
void (QComboBox::*qComboBoxCurrentIndexChanged)(int) = &QComboBox::currentIndexChanged;
@ -175,7 +189,11 @@ OptionsDialog::OptionsDialog(IGUIApplication *app, QWidget *parent)
// setup apply button
m_applyButton->setEnabled(false);
connect(m_applyButton, &QPushButton::clicked, this, &OptionsDialog::applySettings);
connect(m_applyButton, &QPushButton::clicked, this, [this]
{
if (applySettings())
m_applyButton->setEnabled(false);
});
// disable mouse wheel event on widgets to avoid misselection
auto *wheelEventEater = new WheelEventEater(this);
@ -1211,28 +1229,33 @@ void OptionsDialog::loadWebUITabOptions()
m_ui->textWebUIRootFolder->setMode(FileSystemPathEdit::Mode::DirectoryOpen);
m_ui->textWebUIRootFolder->setDialogCaption(tr("Choose Alternative UI files location"));
m_ui->checkWebUi->setChecked(pref->isWebUiEnabled());
m_ui->textWebUiAddress->setText(pref->getWebUiAddress());
m_ui->spinWebUiPort->setValue(pref->getWebUiPort());
if (app()->webUI()->isErrored())
m_ui->labelWebUIError->setText(tr("WebUI configuration failed. Reason: %1").arg(app()->webUI()->errorMessage()));
else
m_ui->labelWebUIError->hide();
m_ui->checkWebUI->setChecked(pref->isWebUIEnabled());
m_ui->textWebUIAddress->setText(pref->getWebUIAddress());
m_ui->spinWebUIPort->setValue(pref->getWebUIPort());
m_ui->checkWebUIUPnP->setChecked(pref->useUPnPForWebUIPort());
m_ui->checkWebUiHttps->setChecked(pref->isWebUiHttpsEnabled());
m_ui->checkWebUIHttps->setChecked(pref->isWebUIHttpsEnabled());
webUIHttpsCertChanged(pref->getWebUIHttpsCertificatePath());
webUIHttpsKeyChanged(pref->getWebUIHttpsKeyPath());
m_ui->textWebUiUsername->setText(pref->getWebUiUsername());
m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUiLocalAuthEnabled());
m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUiAuthSubnetWhitelistEnabled());
m_ui->textWebUIUsername->setText(pref->getWebUIUsername());
m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUILocalAuthEnabled());
m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUIAuthSubnetWhitelistEnabled());
m_ui->IPSubnetWhitelistButton->setEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
m_ui->spinBanCounter->setValue(pref->getWebUIMaxAuthFailCount());
m_ui->spinBanDuration->setValue(pref->getWebUIBanDuration().count());
m_ui->spinSessionTimeout->setValue(pref->getWebUISessionTimeout());
// Alternative UI
m_ui->groupAltWebUI->setChecked(pref->isAltWebUiEnabled());
m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUiRootFolder());
m_ui->groupAltWebUI->setChecked(pref->isAltWebUIEnabled());
m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUIRootFolder());
// Security
m_ui->checkClickjacking->setChecked(pref->isWebUiClickjackingProtectionEnabled());
m_ui->checkCSRFProtection->setChecked(pref->isWebUiCSRFProtectionEnabled());
m_ui->checkSecureCookie->setEnabled(pref->isWebUiHttpsEnabled());
m_ui->checkSecureCookie->setChecked(pref->isWebUiSecureCookieEnabled());
m_ui->checkClickjacking->setChecked(pref->isWebUIClickjackingProtectionEnabled());
m_ui->checkCSRFProtection->setChecked(pref->isWebUICSRFProtectionEnabled());
m_ui->checkSecureCookie->setEnabled(pref->isWebUIHttpsEnabled());
m_ui->checkSecureCookie->setChecked(pref->isWebUISecureCookieEnabled());
m_ui->groupHostHeaderValidation->setChecked(pref->isWebUIHostHeaderValidationEnabled());
m_ui->textServerDomains->setText(pref->getServerDomains());
// Custom HTTP headers
@ -1248,18 +1271,18 @@ void OptionsDialog::loadWebUITabOptions()
m_ui->DNSUsernameTxt->setText(pref->getDynDNSUsername());
m_ui->DNSPasswordTxt->setText(pref->getDynDNSPassword());
connect(m_ui->checkWebUi, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUiAddress, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->spinWebUiPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkWebUI, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIAddress, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->spinWebUIPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkWebUIUPnP, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkWebUiHttps, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkWebUIHttps, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIHttpsCert, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIHttpsCert, &FileSystemPathLineEdit::selectedPathChanged, this, &OptionsDialog::webUIHttpsCertChanged);
connect(m_ui->textWebUIHttpsKey, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIHttpsKey, &FileSystemPathLineEdit::selectedPathChanged, this, &OptionsDialog::webUIHttpsKeyChanged);
connect(m_ui->textWebUiUsername, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUiPassword, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIUsername, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIPassword, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
@ -1273,7 +1296,7 @@ void OptionsDialog::loadWebUITabOptions()
connect(m_ui->checkClickjacking, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkCSRFProtection, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkWebUiHttps, &QGroupBox::toggled, m_ui->checkSecureCookie, &QWidget::setEnabled);
connect(m_ui->checkWebUIHttps, &QGroupBox::toggled, m_ui->checkSecureCookie, &QWidget::setEnabled);
connect(m_ui->checkSecureCookie, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->groupHostHeaderValidation, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textServerDomains, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
@ -1295,29 +1318,32 @@ void OptionsDialog::saveWebUITabOptions() const
{
auto *pref = Preferences::instance();
pref->setWebUiEnabled(isWebUiEnabled());
pref->setWebUiAddress(m_ui->textWebUiAddress->text());
pref->setWebUiPort(m_ui->spinWebUiPort->value());
const bool webUIEnabled = isWebUIEnabled();
pref->setWebUIEnabled(webUIEnabled);
pref->setWebUIAddress(m_ui->textWebUIAddress->text());
pref->setWebUIPort(m_ui->spinWebUIPort->value());
pref->setUPnPForWebUIPort(m_ui->checkWebUIUPnP->isChecked());
pref->setWebUiHttpsEnabled(m_ui->checkWebUiHttps->isChecked());
pref->setWebUIHttpsEnabled(m_ui->checkWebUIHttps->isChecked());
pref->setWebUIHttpsCertificatePath(m_ui->textWebUIHttpsCert->selectedPath());
pref->setWebUIHttpsKeyPath(m_ui->textWebUIHttpsKey->selectedPath());
pref->setWebUIMaxAuthFailCount(m_ui->spinBanCounter->value());
pref->setWebUIBanDuration(std::chrono::seconds {m_ui->spinBanDuration->value()});
pref->setWebUISessionTimeout(m_ui->spinSessionTimeout->value());
// Authentication
pref->setWebUiUsername(webUiUsername());
if (!webUiPassword().isEmpty())
pref->setWebUIPassword(Utils::Password::PBKDF2::generate(webUiPassword()));
pref->setWebUiLocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked());
pref->setWebUiAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
if (const QString username = webUIUsername(); isValidWebUIUsername(username))
pref->setWebUIUsername(username);
if (const QString password = webUIPassword(); isValidWebUIPassword(password))
pref->setWebUIPassword(Utils::Password::PBKDF2::generate(password));
pref->setWebUILocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked());
pref->setWebUIAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
// Alternative UI
pref->setAltWebUiEnabled(m_ui->groupAltWebUI->isChecked());
pref->setWebUiRootFolder(m_ui->textWebUIRootFolder->selectedPath());
pref->setAltWebUIEnabled(m_ui->groupAltWebUI->isChecked());
pref->setWebUIRootFolder(m_ui->textWebUIRootFolder->selectedPath());
// Security
pref->setWebUiClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked());
pref->setWebUiCSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked());
pref->setWebUiSecureCookieEnabled(m_ui->checkSecureCookie->isChecked());
pref->setWebUIClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked());
pref->setWebUICSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked());
pref->setWebUISecureCookieEnabled(m_ui->checkSecureCookie->isChecked());
pref->setWebUIHostHeaderValidationEnabled(m_ui->groupHostHeaderValidation->isChecked());
pref->setServerDomains(m_ui->textServerDomains->text());
// Custom HTTP headers
@ -1517,53 +1543,37 @@ void OptionsDialog::on_buttonBox_accepted()
{
if (m_applyButton->isEnabled())
{
if (!schedTimesOk())
{
m_ui->tabSelection->setCurrentRow(TAB_SPEED);
if (!applySettings())
return;
}
#ifndef DISABLE_WEBUI
if (!webUIAuthenticationOk())
{
m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
return;
}
if (!isAlternativeWebUIPathValid())
{
m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
return;
}
#endif
m_applyButton->setEnabled(false);
saveOptions();
}
accept();
}
void OptionsDialog::applySettings()
bool OptionsDialog::applySettings()
{
if (!schedTimesOk())
{
m_ui->tabSelection->setCurrentRow(TAB_SPEED);
return;
return false;
}
#ifndef DISABLE_WEBUI
if (!webUIAuthenticationOk())
if (isWebUIEnabled() && !webUIAuthenticationOk())
{
m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
return;
return false;
}
if (!isAlternativeWebUIPathValid())
{
m_ui->tabSelection->setCurrentRow(TAB_WEBUI);
return;
return false;
}
#endif
m_applyButton->setEnabled(false);
saveOptions();
return true;
}
void OptionsDialog::on_buttonBox_rejected()
@ -1859,31 +1869,33 @@ void OptionsDialog::webUIHttpsKeyChanged(const Path &path)
(isKeyValid ? u"security-high"_s : u"security-low"_s), 24));
}
bool OptionsDialog::isWebUiEnabled() const
bool OptionsDialog::isWebUIEnabled() const
{
return m_ui->checkWebUi->isChecked();
return m_ui->checkWebUI->isChecked();
}
QString OptionsDialog::webUiUsername() const
QString OptionsDialog::webUIUsername() const
{
return m_ui->textWebUiUsername->text();
return m_ui->textWebUIUsername->text();
}
QString OptionsDialog::webUiPassword() const
QString OptionsDialog::webUIPassword() const
{
return m_ui->textWebUiPassword->text();
return m_ui->textWebUIPassword->text();
}
bool OptionsDialog::webUIAuthenticationOk()
{
if (webUiUsername().length() < 3)
if (!isValidWebUIUsername(webUIUsername()))
{
QMessageBox::warning(this, tr("Length Error"), tr("The Web UI username must be at least 3 characters long."));
QMessageBox::warning(this, tr("Length Error"), tr("The WebUI username must be at least 3 characters long."));
return false;
}
if (!webUiPassword().isEmpty() && (webUiPassword().length() < 6))
const bool dontChangePassword = webUIPassword().isEmpty() && !Preferences::instance()->getWebUIPassword().isEmpty();
if (!isValidWebUIPassword(webUIPassword()) && !dontChangePassword)
{
QMessageBox::warning(this, tr("Length Error"), tr("The Web UI password must be at least 6 characters long."));
QMessageBox::warning(this, tr("Length Error"), tr("The WebUI password must be at least 6 characters long."));
return false;
}
return true;
@ -1893,7 +1905,7 @@ bool OptionsDialog::isAlternativeWebUIPathValid()
{
if (m_ui->groupAltWebUI->isChecked() && m_ui->textWebUIRootFolder->selectedPath().isEmpty())
{
QMessageBox::warning(this, tr("Location Error"), tr("The alternative Web UI files location cannot be blank."));
QMessageBox::warning(this, tr("Location Error"), tr("The alternative WebUI files location cannot be blank."));
return false;
}
return true;

View file

@ -88,7 +88,6 @@ private slots:
void adjustProxyOptions();
void on_buttonBox_accepted();
void on_buttonBox_rejected();
void applySettings();
void enableApplyButton();
void toggleComboRatioLimitAct();
void changePage(QListWidgetItem *, QListWidgetItem *);
@ -115,6 +114,7 @@ private:
void showEvent(QShowEvent *e) override;
// Methods
bool applySettings();
void saveOptions() const;
void loadBehaviorTabOptions();
@ -184,9 +184,9 @@ private:
int getMaxActiveTorrents() const;
// WebUI
#ifndef DISABLE_WEBUI
bool isWebUiEnabled() const;
QString webUiUsername() const;
QString webUiPassword() const;
bool isWebUIEnabled() const;
QString webUIUsername() const;
QString webUIPassword() const;
bool webUIAuthenticationOk();
bool isAlternativeWebUIPathValid();
#endif

View file

@ -3223,8 +3223,8 @@ Disable encryption: Only connect to peers without protocol encryption</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabWebuiPage">
<layout class="QVBoxLayout" name="tabWebuiPageLayout">
<widget class="QWidget" name="tabWebUIPage">
<layout class="QVBoxLayout" name="tabWebUIPageLayout">
<property name="leftMargin">
<number>0</number>
</property>
@ -3253,7 +3253,7 @@ Disable encryption: Only connect to peers without protocol encryption</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_23">
<item>
<widget class="QGroupBox" name="checkWebUi">
<widget class="QGroupBox" name="checkWebUI">
<property name="title">
<string>Web User Interface (Remote control)</string>
</property>
@ -3264,17 +3264,29 @@ Disable encryption: Only connect to peers without protocol encryption</string>
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="labelWebUIError">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="lblWebUiAddress">
<widget class="QLabel" name="lblWebUIAddress">
<property name="text">
<string>IP address:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="textWebUiAddress">
<widget class="QLineEdit" name="textWebUIAddress">
<property name="toolTip">
<string>IP address that the Web UI will bind to.
Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv4 address,
@ -3283,14 +3295,14 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
</widget>
</item>
<item>
<widget class="QLabel" name="lblWebUiPort">
<widget class="QLabel" name="lblWebUIPort">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinWebUiPort">
<widget class="QSpinBox" name="spinWebUIPort">
<property name="minimum">
<number>1</number>
</property>
@ -3315,7 +3327,7 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
</widget>
</item>
<item>
<widget class="QGroupBox" name="checkWebUiHttps">
<widget class="QGroupBox" name="checkWebUIHttps">
<property name="title">
<string>&amp;Use HTTPS instead of HTTP</string>
</property>
@ -3327,14 +3339,14 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
</property>
<layout class="QGridLayout" name="gridLayout_11">
<item row="1" column="1">
<widget class="QLabel" name="lblWebUiKey">
<widget class="QLabel" name="lblWebUIKey">
<property name="text">
<string>Key:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="lblWebUiCrt">
<widget class="QLabel" name="lblWebUICrt">
<property name="text">
<string>Certificate:</string>
</property>
@ -3366,7 +3378,7 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupWebUiAuth">
<widget class="QGroupBox" name="groupWebUIAuth">
<property name="title">
<string>Authentication</string>
</property>
@ -3374,24 +3386,24 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
<item>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<widget class="QLabel" name="lblWebUiUsername">
<widget class="QLabel" name="lblWebUIUsername">
<property name="text">
<string>Username:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="textWebUiUsername"/>
<widget class="QLineEdit" name="textWebUIUsername"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblWebUiPassword">
<widget class="QLabel" name="lblWebUIPassword">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="textWebUiPassword">
<widget class="QLineEdit" name="textWebUIPassword">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
@ -3819,13 +3831,13 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
<tabstop>stopConditionComboBox</tabstop>
<tabstop>spinPort</tabstop>
<tabstop>checkUPnP</tabstop>
<tabstop>textWebUiUsername</tabstop>
<tabstop>checkWebUi</tabstop>
<tabstop>textWebUIUsername</tabstop>
<tabstop>checkWebUI</tabstop>
<tabstop>textSavePath</tabstop>
<tabstop>scrollArea_7</tabstop>
<tabstop>scrollArea_2</tabstop>
<tabstop>spinWebUiPort</tabstop>
<tabstop>textWebUiPassword</tabstop>
<tabstop>spinWebUIPort</tabstop>
<tabstop>textWebUIPassword</tabstop>
<tabstop>buttonBox</tabstop>
<tabstop>tabSelection</tabstop>
<tabstop>scrollArea</tabstop>
@ -3915,7 +3927,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
<tabstop>spinMaxActiveUploads</tabstop>
<tabstop>spinMaxActiveTorrents</tabstop>
<tabstop>checkWebUIUPnP</tabstop>
<tabstop>checkWebUiHttps</tabstop>
<tabstop>checkWebUIHttps</tabstop>
<tabstop>checkBypassLocalAuth</tabstop>
<tabstop>checkBypassAuthSubnetWhitelist</tabstop>
<tabstop>IPSubnetWhitelistButton</tabstop>

View file

@ -92,7 +92,7 @@ void AppController::buildInfoAction()
void AppController::shutdownAction()
{
// Special handling for shutdown, we
// need to reply to the Web UI before
// need to reply to the WebUI before
// actually shutting down.
QTimer::singleShot(100ms, Qt::CoarseTimer, qApp, []
{
@ -275,33 +275,33 @@ void AppController::preferencesAction()
data[u"add_trackers_enabled"_s] = session->isAddTrackersEnabled();
data[u"add_trackers"_s] = session->additionalTrackers();
// Web UI
// WebUI
// HTTP Server
data[u"web_ui_domain_list"_s] = pref->getServerDomains();
data[u"web_ui_address"_s] = pref->getWebUiAddress();
data[u"web_ui_port"_s] = pref->getWebUiPort();
data[u"web_ui_address"_s] = pref->getWebUIAddress();
data[u"web_ui_port"_s] = pref->getWebUIPort();
data[u"web_ui_upnp"_s] = pref->useUPnPForWebUIPort();
data[u"use_https"_s] = pref->isWebUiHttpsEnabled();
data[u"use_https"_s] = pref->isWebUIHttpsEnabled();
data[u"web_ui_https_cert_path"_s] = pref->getWebUIHttpsCertificatePath().toString();
data[u"web_ui_https_key_path"_s] = pref->getWebUIHttpsKeyPath().toString();
// Authentication
data[u"web_ui_username"_s] = pref->getWebUiUsername();
data[u"bypass_local_auth"_s] = !pref->isWebUiLocalAuthEnabled();
data[u"bypass_auth_subnet_whitelist_enabled"_s] = pref->isWebUiAuthSubnetWhitelistEnabled();
data[u"web_ui_username"_s] = pref->getWebUIUsername();
data[u"bypass_local_auth"_s] = !pref->isWebUILocalAuthEnabled();
data[u"bypass_auth_subnet_whitelist_enabled"_s] = pref->isWebUIAuthSubnetWhitelistEnabled();
QStringList authSubnetWhitelistStringList;
for (const Utils::Net::Subnet &subnet : asConst(pref->getWebUiAuthSubnetWhitelist()))
for (const Utils::Net::Subnet &subnet : asConst(pref->getWebUIAuthSubnetWhitelist()))
authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet);
data[u"bypass_auth_subnet_whitelist"_s] = authSubnetWhitelistStringList.join(u'\n');
data[u"web_ui_max_auth_fail_count"_s] = pref->getWebUIMaxAuthFailCount();
data[u"web_ui_ban_duration"_s] = static_cast<int>(pref->getWebUIBanDuration().count());
data[u"web_ui_session_timeout"_s] = pref->getWebUISessionTimeout();
// Use alternative Web UI
data[u"alternative_webui_enabled"_s] = pref->isAltWebUiEnabled();
data[u"alternative_webui_path"_s] = pref->getWebUiRootFolder().toString();
// Use alternative WebUI
data[u"alternative_webui_enabled"_s] = pref->isAltWebUIEnabled();
data[u"alternative_webui_path"_s] = pref->getWebUIRootFolder().toString();
// Security
data[u"web_ui_clickjacking_protection_enabled"_s] = pref->isWebUiClickjackingProtectionEnabled();
data[u"web_ui_csrf_protection_enabled"_s] = pref->isWebUiCSRFProtectionEnabled();
data[u"web_ui_secure_cookie_enabled"_s] = pref->isWebUiSecureCookieEnabled();
data[u"web_ui_clickjacking_protection_enabled"_s] = pref->isWebUIClickjackingProtectionEnabled();
data[u"web_ui_csrf_protection_enabled"_s] = pref->isWebUICSRFProtectionEnabled();
data[u"web_ui_secure_cookie_enabled"_s] = pref->isWebUISecureCookieEnabled();
data[u"web_ui_host_header_validation_enabled"_s] = pref->isWebUIHostHeaderValidationEnabled();
// Custom HTTP headers
data[u"web_ui_use_custom_http_headers_enabled"_s] = pref->isWebUICustomHTTPHeadersEnabled();
@ -782,35 +782,35 @@ void AppController::setPreferencesAction()
if (hasKey(u"add_trackers"_s))
session->setAdditionalTrackers(it.value().toString());
// Web UI
// WebUI
// HTTP Server
if (hasKey(u"web_ui_domain_list"_s))
pref->setServerDomains(it.value().toString());
if (hasKey(u"web_ui_address"_s))
pref->setWebUiAddress(it.value().toString());
pref->setWebUIAddress(it.value().toString());
if (hasKey(u"web_ui_port"_s))
pref->setWebUiPort(it.value().value<quint16>());
pref->setWebUIPort(it.value().value<quint16>());
if (hasKey(u"web_ui_upnp"_s))
pref->setUPnPForWebUIPort(it.value().toBool());
if (hasKey(u"use_https"_s))
pref->setWebUiHttpsEnabled(it.value().toBool());
pref->setWebUIHttpsEnabled(it.value().toBool());
if (hasKey(u"web_ui_https_cert_path"_s))
pref->setWebUIHttpsCertificatePath(Path(it.value().toString()));
if (hasKey(u"web_ui_https_key_path"_s))
pref->setWebUIHttpsKeyPath(Path(it.value().toString()));
// Authentication
if (hasKey(u"web_ui_username"_s))
pref->setWebUiUsername(it.value().toString());
pref->setWebUIUsername(it.value().toString());
if (hasKey(u"web_ui_password"_s))
pref->setWebUIPassword(Utils::Password::PBKDF2::generate(it.value().toByteArray()));
if (hasKey(u"bypass_local_auth"_s))
pref->setWebUiLocalAuthEnabled(!it.value().toBool());
pref->setWebUILocalAuthEnabled(!it.value().toBool());
if (hasKey(u"bypass_auth_subnet_whitelist_enabled"_s))
pref->setWebUiAuthSubnetWhitelistEnabled(it.value().toBool());
pref->setWebUIAuthSubnetWhitelistEnabled(it.value().toBool());
if (hasKey(u"bypass_auth_subnet_whitelist"_s))
{
// recognize new lines and commas as delimiters
pref->setWebUiAuthSubnetWhitelist(it.value().toString().split(QRegularExpression(u"\n|,"_s), Qt::SkipEmptyParts));
pref->setWebUIAuthSubnetWhitelist(it.value().toString().split(QRegularExpression(u"\n|,"_s), Qt::SkipEmptyParts));
}
if (hasKey(u"web_ui_max_auth_fail_count"_s))
pref->setWebUIMaxAuthFailCount(it.value().toInt());
@ -818,18 +818,18 @@ void AppController::setPreferencesAction()
pref->setWebUIBanDuration(std::chrono::seconds {it.value().toInt()});
if (hasKey(u"web_ui_session_timeout"_s))
pref->setWebUISessionTimeout(it.value().toInt());
// Use alternative Web UI
// Use alternative WebUI
if (hasKey(u"alternative_webui_enabled"_s))
pref->setAltWebUiEnabled(it.value().toBool());
pref->setAltWebUIEnabled(it.value().toBool());
if (hasKey(u"alternative_webui_path"_s))
pref->setWebUiRootFolder(Path(it.value().toString()));
pref->setWebUIRootFolder(Path(it.value().toString()));
// Security
if (hasKey(u"web_ui_clickjacking_protection_enabled"_s))
pref->setWebUiClickjackingProtectionEnabled(it.value().toBool());
pref->setWebUIClickjackingProtectionEnabled(it.value().toBool());
if (hasKey(u"web_ui_csrf_protection_enabled"_s))
pref->setWebUiCSRFProtectionEnabled(it.value().toBool());
pref->setWebUICSRFProtectionEnabled(it.value().toBool());
if (hasKey(u"web_ui_secure_cookie_enabled"_s))
pref->setWebUiSecureCookieEnabled(it.value().toBool());
pref->setWebUISecureCookieEnabled(it.value().toBool());
if (hasKey(u"web_ui_host_header_validation_enabled"_s))
pref->setWebUIHostHeaderValidationEnabled(it.value().toBool());
// Custom HTTP headers

View file

@ -43,6 +43,16 @@ AuthController::AuthController(ISessionManager *sessionManager, IApplication *ap
{
}
void AuthController::setUsername(const QString &username)
{
m_username = username;
}
void AuthController::setPasswordHash(const QByteArray &passwordHash)
{
m_passwordHash = passwordHash;
}
void AuthController::loginAction()
{
if (m_sessionManager->session())
@ -51,9 +61,9 @@ void AuthController::loginAction()
return;
}
const QString clientAddr {m_sessionManager->clientId()};
const QString usernameFromWeb {params()[u"username"_s]};
const QString passwordFromWeb {params()[u"password"_s]};
const QString clientAddr = m_sessionManager->clientId();
const QString usernameFromWeb = params()[u"username"_s];
const QString passwordFromWeb = params()[u"password"_s];
if (isBanned())
{
@ -61,15 +71,11 @@ void AuthController::loginAction()
.arg(clientAddr, usernameFromWeb)
, Log::WARNING);
throw APIError(APIErrorType::AccessDenied
, tr("Your IP address has been banned after too many failed authentication attempts."));
, tr("Your IP address has been banned after too many failed authentication attempts."));
}
const Preferences *pref = Preferences::instance();
const QString username {pref->getWebUiUsername()};
const QByteArray secret {pref->getWebUIPassword()};
const bool usernameEqual = Utils::Password::slowEquals(usernameFromWeb.toUtf8(), username.toUtf8());
const bool passwordEqual = Utils::Password::PBKDF2::verify(secret, passwordFromWeb);
const bool usernameEqual = Utils::Password::slowEquals(usernameFromWeb.toUtf8(), m_username.toUtf8());
const bool passwordEqual = Utils::Password::PBKDF2::verify(m_passwordHash, passwordFromWeb);
if (usernameEqual && passwordEqual)
{

View file

@ -28,8 +28,10 @@
#pragma once
#include <QByteArray>
#include <QDeadlineTimer>
#include <QHash>
#include <QString>
#include "apicontroller.h"
@ -45,6 +47,9 @@ class AuthController : public APIController
public:
explicit AuthController(ISessionManager *sessionManager, IApplication *app, QObject *parent = nullptr);
void setUsername(const QString &username);
void setPasswordHash(const QByteArray &passwordHash);
private slots:
void loginAction();
void logoutAction() const;
@ -56,6 +61,9 @@ private:
ISessionManager *m_sessionManager = nullptr;
QString m_username;
QByteArray m_passwordHash;
struct FailedLogin
{
int failedAttemptsCount = 0;

View file

@ -269,6 +269,16 @@ const Http::Environment &WebApplication::env() const
return m_env;
}
void WebApplication::setUsername(const QString &username)
{
m_authController->setUsername(username);
}
void WebApplication::setPasswordHash(const QByteArray &passwordHash)
{
m_authController->setPasswordHash(passwordHash);
}
void WebApplication::doProcessRequest()
{
const QRegularExpressionMatch match = m_apiPathPattern.match(request().path);
@ -357,17 +367,17 @@ void WebApplication::configure()
{
const auto *pref = Preferences::instance();
const bool isAltUIUsed = pref->isAltWebUiEnabled();
const Path rootFolder = (!isAltUIUsed ? Path(WWW_FOLDER) : pref->getWebUiRootFolder());
const bool isAltUIUsed = pref->isAltWebUIEnabled();
const Path rootFolder = (!isAltUIUsed ? Path(WWW_FOLDER) : pref->getWebUIRootFolder());
if ((isAltUIUsed != m_isAltUIUsed) || (rootFolder != m_rootFolder))
{
m_isAltUIUsed = isAltUIUsed;
m_rootFolder = rootFolder;
m_translatedFiles.clear();
if (!m_isAltUIUsed)
LogMsg(tr("Using built-in Web UI."));
LogMsg(tr("Using built-in WebUI."));
else
LogMsg(tr("Using custom Web UI. Location: \"%1\".").arg(m_rootFolder.toString()));
LogMsg(tr("Using custom WebUI. Location: \"%1\".").arg(m_rootFolder.toString()));
}
const QString newLocale = pref->getLocale();
@ -379,27 +389,27 @@ void WebApplication::configure()
m_translationFileLoaded = m_translator.load((m_rootFolder / Path(u"translations/webui_"_s) + newLocale).data());
if (m_translationFileLoaded)
{
LogMsg(tr("Web UI translation for selected locale (%1) has been successfully loaded.")
LogMsg(tr("WebUI translation for selected locale (%1) has been successfully loaded.")
.arg(newLocale));
}
else
{
LogMsg(tr("Couldn't load Web UI translation for selected locale (%1).").arg(newLocale), Log::WARNING);
LogMsg(tr("Couldn't load WebUI translation for selected locale (%1).").arg(newLocale), Log::WARNING);
}
}
m_isLocalAuthEnabled = pref->isWebUiLocalAuthEnabled();
m_isAuthSubnetWhitelistEnabled = pref->isWebUiAuthSubnetWhitelistEnabled();
m_authSubnetWhitelist = pref->getWebUiAuthSubnetWhitelist();
m_isLocalAuthEnabled = pref->isWebUILocalAuthEnabled();
m_isAuthSubnetWhitelistEnabled = pref->isWebUIAuthSubnetWhitelistEnabled();
m_authSubnetWhitelist = pref->getWebUIAuthSubnetWhitelist();
m_sessionTimeout = pref->getWebUISessionTimeout();
m_domainList = pref->getServerDomains().split(u';', Qt::SkipEmptyParts);
std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); });
m_isCSRFProtectionEnabled = pref->isWebUiCSRFProtectionEnabled();
m_isSecureCookieEnabled = pref->isWebUiSecureCookieEnabled();
m_isCSRFProtectionEnabled = pref->isWebUICSRFProtectionEnabled();
m_isSecureCookieEnabled = pref->isWebUISecureCookieEnabled();
m_isHostHeaderValidationEnabled = pref->isWebUIHostHeaderValidationEnabled();
m_isHttpsEnabled = pref->isWebUiHttpsEnabled();
m_isHttpsEnabled = pref->isWebUIHttpsEnabled();
m_prebuiltHeaders.clear();
m_prebuiltHeaders.push_back({Http::HEADER_X_XSS_PROTECTION, u"1; mode=block"_s});
@ -411,7 +421,7 @@ void WebApplication::configure()
m_prebuiltHeaders.push_back({Http::HEADER_REFERRER_POLICY, u"same-origin"_s});
}
const bool isClickjackingProtectionEnabled = pref->isWebUiClickjackingProtectionEnabled();
const bool isClickjackingProtectionEnabled = pref->isWebUIClickjackingProtectionEnabled();
if (isClickjackingProtectionEnabled)
m_prebuiltHeaders.push_back({Http::HEADER_X_FRAME_OPTIONS, u"SAMEORIGIN"_s});

View file

@ -105,6 +105,9 @@ public:
const Http::Request &request() const;
const Http::Environment &env() const;
void setUsername(const QString &username);
void setPasswordHash(const QByteArray &passwordHash);
private:
void doProcessRequest();
void configure();

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015, 2023 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -28,6 +28,7 @@
#include "webui.h"
#include "base/global.h"
#include "base/http/server.h"
#include "base/logger.h"
#include "base/net/dnsupdater.h"
@ -36,10 +37,12 @@
#include "base/preferences.h"
#include "base/utils/io.h"
#include "base/utils/net.h"
#include "base/utils/password.h"
#include "webapplication.h"
WebUI::WebUI(IApplication *app)
WebUI::WebUI(IApplication *app, const QByteArray &tempPasswordHash)
: ApplicationComponent(app)
, m_passwordHash {tempPasswordHash}
{
configure();
connect(Preferences::instance(), &Preferences::changed, this, &WebUI::configure);
@ -49,12 +52,23 @@ void WebUI::configure()
{
m_isErrored = false; // clear previous error state
const QString portForwardingProfile = u"webui"_s;
const Preferences *pref = Preferences::instance();
const quint16 port = pref->getWebUiPort();
const bool isEnabled = pref->isWebUIEnabled();
const QString username = pref->getWebUIUsername();
if (const QByteArray passwordHash = pref->getWebUIPassword(); !passwordHash.isEmpty())
m_passwordHash = passwordHash;
if (pref->isWebUiEnabled())
if (isEnabled && (username.isEmpty() || m_passwordHash.isEmpty()))
{
setError(tr("Credentials are not set"));
}
const QString portForwardingProfile = u"webui"_s;
if (isEnabled && !m_isErrored)
{
const quint16 port = pref->getWebUIPort();
// Port forwarding
auto *portForwarder = Net::PortForwarder::instance();
if (pref->useUPnPForWebUIPort())
@ -67,7 +81,7 @@ void WebUI::configure()
}
// http server
const QString serverAddressString = pref->getWebUiAddress();
const QString serverAddressString = pref->getWebUIAddress();
const auto serverAddress = ((serverAddressString == u"*") || serverAddressString.isEmpty())
? QHostAddress::Any : QHostAddress(serverAddressString);
@ -82,7 +96,10 @@ void WebUI::configure()
m_httpServer->close();
}
if (pref->isWebUiHttpsEnabled())
m_webapp->setUsername(username);
m_webapp->setPasswordHash(m_passwordHash);
if (pref->isWebUIHttpsEnabled())
{
const auto readData = [](const Path &path) -> QByteArray
{
@ -94,9 +111,9 @@ void WebUI::configure()
const bool success = m_httpServer->setupHttps(cert, key);
if (success)
LogMsg(tr("Web UI: HTTPS setup successful"));
LogMsg(tr("WebUI: HTTPS setup successful"));
else
LogMsg(tr("Web UI: HTTPS setup failed, fallback to HTTP"), Log::CRITICAL);
LogMsg(tr("WebUI: HTTPS setup failed, fallback to HTTP"), Log::CRITICAL);
}
else
{
@ -108,17 +125,12 @@ void WebUI::configure()
const bool success = m_httpServer->listen(serverAddress, port);
if (success)
{
LogMsg(tr("Web UI: Now listening on IP: %1, port: %2").arg(serverAddressString).arg(port));
LogMsg(tr("WebUI: Now listening on IP: %1, port: %2").arg(serverAddressString).arg(port));
}
else
{
const QString errorMsg = tr("Web UI: Unable to bind to IP: %1, port: %2. Reason: %3")
.arg(serverAddressString).arg(port).arg(m_httpServer->errorString());
LogMsg(errorMsg, Log::CRITICAL);
qCritical() << errorMsg;
m_isErrored = true;
emit fatalError();
setError(tr("Unable to bind to IP: %1, port: %2. Reason: %3")
.arg(serverAddressString).arg(port).arg(m_httpServer->errorString()));
}
}
@ -145,7 +157,24 @@ void WebUI::configure()
}
}
void WebUI::setError(const QString &message)
{
m_isErrored = true;
m_errorMsg = message;
const QString logMessage = u"WebUI: " + m_errorMsg;
LogMsg(logMessage, Log::CRITICAL);
qCritical() << logMessage;
emit fatalError();
}
bool WebUI::isErrored() const
{
return m_isErrored;
}
QString WebUI::errorMessage() const
{
return m_errorMsg;
}

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015, 2023 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -51,9 +51,10 @@ class WebUI : public QObject, public ApplicationComponent
Q_DISABLE_COPY_MOVE(WebUI)
public:
explicit WebUI(IApplication *app);
explicit WebUI(IApplication *app, const QByteArray &tempPasswordHash = {});
bool isErrored() const;
QString errorMessage() const;
signals:
void fatalError();
@ -62,8 +63,13 @@ private slots:
void configure();
private:
void setError(const QString &message);
bool m_isErrored = false;
QString m_errorMsg;
QPointer<Http::Server> m_httpServer;
QPointer<Net::DNSUpdater> m_dnsUpdater;
QPointer<WebApplication> m_webapp;
QByteArray m_passwordHash;
};