From 0f40fad74d6e366b16195bbf89659a0eb1cd131e Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Fri, 10 Nov 2023 07:18:42 +0300 Subject: [PATCH] Drop WebUI default credentials PR #19777. --- src/app/application.cpp | 37 +++-- src/app/application.h | 3 + src/app/cmdoptions.cpp | 8 +- src/app/cmdoptions.h | 2 +- src/base/interfaces/iapplication.h | 4 + src/base/preferences.cpp | 80 ++++++----- src/base/preferences.h | 52 +++---- src/base/utils/password.cpp | 22 ++- src/base/utils/password.h | 3 + src/gui/ipsubnetwhitelistoptionsdialog.cpp | 4 +- src/gui/mainwindow.cpp | 2 +- src/gui/optionsdialog.cpp | 150 +++++++++++---------- src/gui/optionsdialog.h | 8 +- src/gui/optionsdialog.ui | 52 ++++--- src/webui/api/appcontroller.cpp | 58 ++++---- src/webui/api/authcontroller.cpp | 26 ++-- src/webui/api/authcontroller.h | 8 ++ src/webui/webapplication.cpp | 36 +++-- src/webui/webapplication.h | 3 + src/webui/webui.cpp | 59 +++++--- src/webui/webui.h | 8 +- 21 files changed, 372 insertions(+), 253 deletions(-) diff --git a/src/app/application.cpp b/src/app/application.cpp index a715b60fc..af8d92b63 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -98,12 +98,14 @@ #include "gui/mainwindow.h" #include "gui/shutdownconfirmdialog.h" #include "gui/uithememanager.h" -#include "gui/utils.h" #include "gui/windowstate.h" #endif // DISABLE_GUI #ifndef DISABLE_WEBUI #include "webui/webui.h" +#ifdef DISABLE_GUI +#include "base/utils/password.h" +#endif #endif namespace @@ -305,8 +307,8 @@ Application::Application(int &argc, char **argv) if (isFileLoggerEnabled()) m_fileLogger = new FileLogger(fileLoggerPath(), isFileLoggerBackup(), fileLoggerMaxSize(), isFileLoggerDeleteOld(), fileLoggerAge(), static_cast(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 { @@ -885,9 +887,18 @@ int Application::exec() #endif // DISABLE_GUI #ifndef DISABLE_WEBUI +#ifndef DISABLE_GUI m_webui = new WebUI(this); -#ifdef DISABLE_GUI - connect(m_webui, &WebUI::error, this, [](const QString &message) { fprintf(stderr, "%s\n", qUtf8Printable(message)); }); +#else + const auto *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())); + connect(m_webui, &WebUI::error, this, [](const QString &message) + { + fprintf(stderr, "WebUI configuration failed. Reason: %s\n", qUtf8Printable(message)); + }); printf("%s", qUtf8Printable(u"\n******** %1 ********\n"_s.arg(tr("Information")))); @@ -905,12 +916,11 @@ int Application::exec() , QString::number(m_webui->port())); printf("%s\n", qUtf8Printable(tr("To control qBittorrent, access the WebUI at: %1").arg(url))); - const Preferences *pref = Preferences::instance(); - 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)); } } @@ -1357,3 +1367,10 @@ AddTorrentManagerImpl *Application::addTorrentManager() const { return m_addTorrentManager; } + +#ifndef DISABLE_WEBUI +WebUI *Application::webUI() const +{ + return m_webui; +} +#endif diff --git a/src/app/application.h b/src/app/application.h index 75601697e..ff7c4ace4 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -153,6 +153,9 @@ private slots: private: AddTorrentManagerImpl *addTorrentManager() const override; +#ifndef DISABLE_WEBUI + WebUI *webUI() const override; +#endif void initializeTranslation(); void processParams(const QBtCommandLineParameters ¶ms); diff --git a/src/app/cmdoptions.cpp b/src/app/cmdoptions.cpp index 460b33d73..5b1d957b6 100644 --- a/src/app/cmdoptions.cpp +++ b/src/app/cmdoptions.cpp @@ -329,7 +329,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)) @@ -367,8 +367,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)); } @@ -489,7 +489,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")) diff --git a/src/app/cmdoptions.h b/src/app/cmdoptions.h index 8817f4708..1c133c413 100644 --- a/src/app/cmdoptions.h +++ b/src/app/cmdoptions.h @@ -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 skipDialog; Path profileDir; diff --git a/src/base/interfaces/iapplication.h b/src/base/interfaces/iapplication.h index c26f6a524..5f8eb18ea 100644 --- a/src/base/interfaces/iapplication.h +++ b/src/base/interfaces/iapplication.h @@ -36,6 +36,7 @@ #include "base/pathfwd.h" class AddTorrentManager; +class WebUI; struct QBtCommandLineParameters; #ifdef Q_OS_WIN @@ -85,4 +86,7 @@ public: #endif virtual AddTorrentManager *addTorrentManager() const = 0; +#ifndef DISABLE_WEBUI + virtual WebUI *webUI() const = 0; +#endif }; diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 559a4e245..8dd1b4f87 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -629,7 +629,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; @@ -639,41 +639,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 Preferences::getWebUiAuthSubnetWhitelist() const +QVector Preferences::getWebUIAuthSubnetWhitelist() const { const auto subnets = value(u"Preferences/WebUI/AuthSubnetWhitelist"_s); @@ -690,7 +690,7 @@ QVector Preferences::getWebUiAuthSubnetWhitelist() const return ret; } -void Preferences::setWebUiAuthSubnetWhitelist(QStringList subnets) +void Preferences::setWebUIAuthSubnetWhitelist(QStringList subnets) { subnets.removeIf([](const QString &subnet) { @@ -713,27 +713,27 @@ void Preferences::setServerDomains(const QString &str) setValue(u"Preferences/WebUI/ServerDomains"_s, str); } -QString Preferences::getWebUiAddress() const +QString Preferences::getWebUIAddress() const { return value(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(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 @@ -753,14 +753,14 @@ void Preferences::setUPnPForWebUIPort(const bool enabled) setValue(u"Preferences/WebUI/UseUPnP"_s, enabled); } -QString Preferences::getWebUiUsername() const +QString Preferences::getWebUIUsername() const { return value(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); @@ -768,9 +768,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(u"Preferences/WebUI/Password_PBKDF2"_s); } void Preferences::setWebUIPassword(const QByteArray &password) @@ -833,40 +831,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); @@ -885,14 +883,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); @@ -924,27 +922,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(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); diff --git a/src/base/preferences.h b/src/base/preferences.h index ac92f55e2..f7af048a8 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -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 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 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; diff --git a/src/base/utils/password.cpp b/src/base/utils/password.cpp index ce136efe5..bec752530 100644 --- a/src/base/utils/password.cpp +++ b/src/base/utils/password.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2018 Mike Tzou (Chocobo1) * * This program is free software; you can redistribute it and/or @@ -36,6 +37,7 @@ #include #include +#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 salt - {{Random::rand(), Random::rand() - , Random::rand(), Random::rand()}}; + const std::array salt { + {Random::rand(), Random::rand(), Random::rand(), Random::rand()}}; std::array outBuf {}; const int hmacResult = PKCS5_PBKDF2_HMAC(password.constData(), password.size() diff --git a/src/base/utils/password.h b/src/base/utils/password.h index 3ad6d8578..b656731e9 100644 --- a/src/base/utils/password.h +++ b/src/base/utils/password.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * 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); diff --git a/src/gui/ipsubnetwhitelistoptionsdialog.cpp b/src/gui/ipsubnetwhitelistoptionsdialog.cpp index 78af3610f..11556422c 100644 --- a/src/gui/ipsubnetwhitelistoptionsdialog.cpp +++ b/src/gui/ipsubnetwhitelistoptionsdialog.cpp @@ -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 diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index f094c704d..ef8458ef8 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -1145,7 +1145,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); diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index a4a935085..02d7ff2f3 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -71,6 +71,7 @@ #include "utils.h" #include "watchedfolderoptionsdialog.h" #include "watchedfoldersmodel.h" +#include "webui/webui.h" #ifndef DISABLE_WEBUI #include "base/net/dnsupdater.h" @@ -82,6 +83,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() @@ -108,6 +112,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; @@ -176,7 +190,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); @@ -1218,28 +1236,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 @@ -1255,18 +1278,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); @@ -1280,7 +1303,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); @@ -1302,29 +1325,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 @@ -1524,53 +1550,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() @@ -1866,31 +1876,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; @@ -1900,7 +1912,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; diff --git a/src/gui/optionsdialog.h b/src/gui/optionsdialog.h index e4d8d14b7..9fd26dafa 100644 --- a/src/gui/optionsdialog.h +++ b/src/gui/optionsdialog.h @@ -89,7 +89,6 @@ private slots: void adjustProxyOptions(); void on_buttonBox_accepted(); void on_buttonBox_rejected(); - void applySettings(); void enableApplyButton(); void toggleComboRatioLimitAct(); void changePage(QListWidgetItem *, QListWidgetItem *); @@ -116,6 +115,7 @@ private: void showEvent(QShowEvent *e) override; // Methods + bool applySettings(); void saveOptions() const; void loadBehaviorTabOptions(); @@ -185,9 +185,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 diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index e278a3da4..ac6778ff8 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -3266,8 +3266,8 @@ Disable encryption: Only connect to peers without protocol encryption - - + + 0 @@ -3296,7 +3296,7 @@ Disable encryption: Only connect to peers without protocol encryption - + Web User Interface (Remote control) @@ -3307,17 +3307,29 @@ Disable encryption: Only connect to peers without protocol encryption false + + + + + true + + + + + + + - + IP address: - + IP address that the Web UI will bind to. Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv4 address, @@ -3326,14 +3338,14 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + Port: - + 1 @@ -3358,7 +3370,7 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + &Use HTTPS instead of HTTP @@ -3370,14 +3382,14 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + Key: - + Certificate: @@ -3409,7 +3421,7 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + Authentication @@ -3417,24 +3429,24 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - + Username: - + - + Password: - + QLineEdit::Password @@ -3862,13 +3874,13 @@ Use ';' to split multiple entries. Can use wildcard '*'. stopConditionComboBox spinPort checkUPnP - textWebUiUsername - checkWebUi + textWebUIUsername + checkWebUI textSavePath scrollArea_7 scrollArea_2 - spinWebUiPort - textWebUiPassword + spinWebUIPort + textWebUIPassword buttonBox tabSelection scrollArea @@ -3958,7 +3970,7 @@ Use ';' to split multiple entries. Can use wildcard '*'. spinMaxActiveUploads spinMaxActiveTorrents checkWebUIUPnP - checkWebUiHttps + checkWebUIHttps checkBypassLocalAuth checkBypassAuthSubnetWhitelist IPSubnetWhitelistButton diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index bdd4c65f0..a99c8d181 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -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(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(); @@ -788,35 +788,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()); + pref->setWebUIPort(it.value().value()); 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()); @@ -824,18 +824,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 diff --git a/src/webui/api/authcontroller.cpp b/src/webui/api/authcontroller.cpp index 24b4ce78e..eb1d1baf2 100644 --- a/src/webui/api/authcontroller.cpp +++ b/src/webui/api/authcontroller.cpp @@ -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) { diff --git a/src/webui/api/authcontroller.h b/src/webui/api/authcontroller.h index c44d68753..0a47c2338 100644 --- a/src/webui/api/authcontroller.h +++ b/src/webui/api/authcontroller.h @@ -28,8 +28,10 @@ #pragma once +#include #include #include +#include #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; diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index a9ec8ebb4..49963a7cf 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -290,6 +290,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); @@ -378,17 +388,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(); @@ -400,27 +410,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}); @@ -432,7 +442,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}); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 14f1e014a..15549eb5c 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -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: QString clientId() const override; WebSession *session() override; diff --git a/src/webui/webui.cpp b/src/webui/webui.cpp index c9e112759..20036c156 100644 --- a/src/webui/webui.cpp +++ b/src/webui/webui.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2015, 2023 Vladimir Golovnev * * 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); @@ -50,13 +53,23 @@ void WebUI::configure() m_isErrored = false; // clear previous error state m_errorMsg.clear(); - const QString portForwardingProfile = u"webui"_s; const Preferences *pref = Preferences::instance(); - const quint16 port = pref->getWebUiPort(); - m_isEnabled = pref->isWebUiEnabled(); + m_isEnabled = pref->isWebUIEnabled(); + const QString username = pref->getWebUIUsername(); + if (const QByteArray passwordHash = pref->getWebUIPassword(); !passwordHash.isEmpty()) + m_passwordHash = passwordHash; - if (m_isEnabled) + if (m_isEnabled && (username.isEmpty() || m_passwordHash.isEmpty())) { + setError(tr("Credentials are not set")); + } + + const QString portForwardingProfile = u"webui"_s; + + if (m_isEnabled && !m_isErrored) + { + const quint16 port = pref->getWebUIPort(); + // Port forwarding auto *portForwarder = Net::PortForwarder::instance(); if (pref->useUPnPForWebUIPort()) @@ -69,7 +82,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); @@ -84,7 +97,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 { @@ -96,9 +112,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 { @@ -110,17 +126,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 { - m_errorMsg = tr("Web UI: Unable to bind to IP: %1, port: %2. Reason: %3") - .arg(serverAddressString).arg(port).arg(m_httpServer->errorString()); - LogMsg(m_errorMsg, Log::CRITICAL); - qCritical() << m_errorMsg; - - m_isErrored = true; - emit error(m_errorMsg); + setError(tr("Unable to bind to IP: %1, port: %2. Reason: %3") + .arg(serverAddressString).arg(port).arg(m_httpServer->errorString())); } } @@ -147,6 +158,18 @@ 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 error(m_errorMsg); +} + bool WebUI::isEnabled() const { return m_isEnabled; diff --git a/src/webui/webui.h b/src/webui/webui.h index 026ec5952..fa0589c2a 100644 --- a/src/webui/webui.h +++ b/src/webui/webui.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2015, 2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -52,7 +52,7 @@ class WebUI final : public ApplicationComponent Q_DISABLE_COPY_MOVE(WebUI) public: - explicit WebUI(IApplication *app); + explicit WebUI(IApplication *app, const QByteArray &tempPasswordHash = {}); bool isEnabled() const; bool isErrored() const; @@ -68,10 +68,14 @@ private slots: void configure(); private: + void setError(const QString &message); + bool m_isEnabled = false; bool m_isErrored = false; QString m_errorMsg; QPointer m_httpServer; QPointer m_dnsUpdater; QPointer m_webapp; + + QByteArray m_passwordHash; };