Backport changes to v4.6.x branch

PR #19751.
This commit is contained in:
Vladimir Golovnev 2023-11-13 14:29:43 +03:00 committed by GitHub
commit 267d504ec0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 717 additions and 397 deletions

View file

@ -90,34 +90,35 @@ jobs:
--recurse-submodules `
https://github.com/arvidn/libtorrent.git
cd libtorrent
$env:CXXFLAGS+=" /guard:cf"
$env:LDFLAGS+=" /guard:cf"
cmake `
-B build `
-G "Ninja" `
-DCMAKE_BUILD_TYPE=RelWithDebInfo `
-DCMAKE_CXX_FLAGS=/guard:cf `
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON `
-DCMAKE_INSTALL_PREFIX="${{ env.libtorrent_path }}" `
-DCMAKE_TOOLCHAIN_FILE="${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" `
-DBOOST_ROOT="${{ env.boost_path }}" `
-DBUILD_SHARED_LIBS=OFF `
-Ddeprecated-functions=OFF `
-Dstatic_runtime=ON `
-Dstatic_runtime=OFF `
-DVCPKG_TARGET_TRIPLET=x64-windows-static-release
cmake --build build
cmake --install build
- name: Build qBittorrent
run: |
$env:CXXFLAGS+=" /WX"
cmake `
-B build `
-G "Ninja" `
-DCMAKE_BUILD_TYPE=RelWithDebInfo `
-DCMAKE_CXX_FLAGS="/WX" `
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON `
-DCMAKE_TOOLCHAIN_FILE="${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" `
-DBOOST_ROOT="${{ env.boost_path }}" `
-DLibtorrentRasterbar_DIR="${{ env.libtorrent_path }}/lib/cmake/LibtorrentRasterbar" `
-DMSVC_RUNTIME_DYNAMIC=OFF `
-DMSVC_RUNTIME_DYNAMIC=ON `
-DQT6=ON `
-DTESTING=ON `
-DVCPKG_TARGET_TRIPLET=x64-windows-static-release `

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
{
@ -375,7 +377,7 @@ void Application::setMemoryWorkingSetLimit(const int size)
return;
m_storeMemoryWorkingSetLimit = size;
#ifdef QBT_USES_LIBTORRENT2
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
applyMemoryWorkingSetLimit();
#endif
}
@ -773,7 +775,7 @@ int Application::exec()
printf("%s\n", qUtf8Printable(loadingStr));
#endif
#ifdef QBT_USES_LIBTORRENT2
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
applyMemoryWorkingSetLimit();
#endif
@ -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
@ -1080,7 +1085,7 @@ void Application::shutdownCleanup(QSessionManager &manager)
}
#endif
#ifdef QBT_USES_LIBTORRENT2
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
void Application::applyMemoryWorkingSetLimit() const
{
const size_t MiB = 1024 * 1024;
@ -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,12 +149,16 @@ 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;
void sendNotificationEmail(const BitTorrent::Torrent *torrent);
#ifdef QBT_USES_LIBTORRENT2
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
void applyMemoryWorkingSetLimit() const;
#endif

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

@ -2435,6 +2435,11 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
return false;
const lt::torrent_handle nativeHandle = downloadedMetadataIter.value();
m_downloadedMetadata.erase(downloadedMetadataIter);
if (!nativeHandle.is_valid())
return true;
#ifdef QBT_USES_LIBTORRENT2
const InfoHash infoHash {nativeHandle.info_hashes()};
if (infoHash.isHybrid())
@ -2445,7 +2450,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
m_downloadedMetadata.remove((altID == downloadedMetadataIter.key()) ? id : altID);
}
#endif
m_downloadedMetadata.erase(downloadedMetadataIter);
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files);
return true;
}

View file

@ -1811,6 +1811,7 @@ void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext cont
{
if (!hasMetadata())
{
m_savePath = newPath;
m_session->handleTorrentSavePathChanged(this);
return;
}

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

@ -30,6 +30,7 @@
#include "addnewtorrentdialog.h"
#include <algorithm>
#include <functional>
#include <QAction>
#include <QDateTime>
@ -141,10 +142,11 @@ class AddNewTorrentDialog::TorrentContentAdaptor final
{
public:
TorrentContentAdaptor(BitTorrent::TorrentInfo &torrentInfo, PathList &filePaths
, QVector<BitTorrent::DownloadPriority> &filePriorities)
, QVector<BitTorrent::DownloadPriority> &filePriorities, std::function<void ()> onFilePrioritiesChanged)
: m_torrentInfo {torrentInfo}
, m_filePaths {filePaths}
, m_filePriorities {filePriorities}
, m_onFilePrioritiesChanged {std::move(onFilePrioritiesChanged)}
{
Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount()));
@ -255,6 +257,8 @@ public:
{
Q_ASSERT(priorities.size() == filesCount());
m_filePriorities = priorities;
if (m_onFilePrioritiesChanged)
m_onFilePrioritiesChanged();
}
Path actualStorageLocation() const override
@ -275,6 +279,7 @@ private:
BitTorrent::TorrentInfo &m_torrentInfo;
PathList &m_filePaths;
QVector<BitTorrent::DownloadPriority> &m_filePriorities;
std::function<void ()> m_onFilePrioritiesChanged;
Path m_originalRootFolder;
BitTorrent::TorrentContentLayout m_currentContentLayout;
};
@ -767,7 +772,7 @@ void AddNewTorrentDialog::contentLayoutChanged()
const auto contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
m_contentAdaptor->applyContentLayout(contentLayout);
m_ui->contentTreeView->setContentHandler(m_contentAdaptor); // to cause reloading
m_ui->contentTreeView->setContentHandler(m_contentAdaptor.get()); // to cause reloading
}
void AddNewTorrentDialog::saveTorrentFile()
@ -992,7 +997,8 @@ void AddNewTorrentDialog::setupTreeview()
if (m_torrentParams.filePaths.isEmpty())
m_torrentParams.filePaths = m_torrentInfo.filePaths();
m_contentAdaptor = new TorrentContentAdaptor(m_torrentInfo, m_torrentParams.filePaths, m_torrentParams.filePriorities);
m_contentAdaptor = std::make_unique<TorrentContentAdaptor>(m_torrentInfo, m_torrentParams.filePaths
, m_torrentParams.filePriorities, [this] { updateDiskSpaceLabel(); });
const auto contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
m_contentAdaptor->applyContentLayout(contentLayout);
@ -1013,7 +1019,7 @@ void AddNewTorrentDialog::setupTreeview()
m_contentAdaptor->prioritizeFiles(priorities);
}
m_ui->contentTreeView->setContentHandler(m_contentAdaptor);
m_ui->contentTreeView->setContentHandler(m_contentAdaptor.get());
m_filterLine->blockSignals(false);

View file

@ -116,7 +116,7 @@ private:
void showEvent(QShowEvent *event) override;
Ui::AddNewTorrentDialog *m_ui = nullptr;
TorrentContentAdaptor *m_contentAdaptor = nullptr;
std::unique_ptr<TorrentContentAdaptor> m_contentAdaptor;
BitTorrent::MagnetUri m_magnetURI;
BitTorrent::TorrentInfo m_torrentInfo;
int m_savePathIndex = -1;

View file

@ -63,7 +63,7 @@ namespace
// qBittorrent section
QBITTORRENT_HEADER,
RESUME_DATA_STORAGE,
#ifdef QBT_USES_LIBTORRENT2
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
MEMORY_WORKING_SET_LIMIT,
#endif
#if defined(Q_OS_WIN)
@ -195,7 +195,7 @@ void AdvancedSettings::saveAdvancedSettings() const
BitTorrent::Session *const session = BitTorrent::Session::instance();
session->setResumeDataStorageType(m_comboBoxResumeDataStorage.currentData().value<BitTorrent::ResumeDataStorageType>());
#ifdef QBT_USES_LIBTORRENT2
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
// Physical memory (RAM) usage limit
app()->setMemoryWorkingSetLimit(m_spinBoxMemoryWorkingSetLimit.value());
#endif
@ -451,7 +451,7 @@ void AdvancedSettings::loadAdvancedSettings()
m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType())));
addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage);
#ifdef QBT_USES_LIBTORRENT2
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
// Physical memory (RAM) usage limit
m_spinBoxMemoryWorkingSetLimit.setMinimum(1);
m_spinBoxMemoryWorkingSetLimit.setMaximum(std::numeric_limits<int>::max());

View file

@ -30,6 +30,7 @@
#include <libtorrent/config.hpp>
#include <QtGlobal>
#include <QCheckBox>
#include <QComboBox>
#include <QLineEdit>
@ -88,7 +89,11 @@ private:
QCheckBox m_checkBoxCoalesceRW;
#else
QComboBox m_comboBoxDiskIOType;
QSpinBox m_spinBoxMemoryWorkingSetLimit, m_spinBoxHashingThreads;
QSpinBox m_spinBoxHashingThreads;
#endif
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
QSpinBox m_spinBoxMemoryWorkingSetLimit;
#endif
#if defined(QBT_USES_LIBTORRENT2) && TORRENT_USE_I2P

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

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -30,13 +31,12 @@
#include <QModelIndex>
#include <QPainter>
#include <QStyleOptionViewItem>
#include "base/utils/misc.h"
#include "base/utils/string.h"
#include "previewselectdialog.h"
PreviewListDelegate::PreviewListDelegate(QObject *parent)
: QItemDelegate(parent)
: QStyledItemDelegate(parent)
{
}
@ -44,15 +44,8 @@ void PreviewListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o
{
painter->save();
QStyleOptionViewItem opt = QItemDelegate::setOptions(index, option);
drawBackground(painter, opt, index);
switch (index.column())
{
case PreviewSelectDialog::SIZE:
QItemDelegate::drawDisplay(painter, opt, option.rect, Utils::Misc::friendlyUnit(index.data().toLongLong()));
break;
case PreviewSelectDialog::PROGRESS:
{
const qreal progress = (index.data().toReal() * 100);
@ -65,7 +58,7 @@ void PreviewListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o
break;
default:
QItemDelegate::paint(painter, option, index);
QStyledItemDelegate::paint(painter, option, index);
break;
}

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -28,11 +29,11 @@
#pragma once
#include <QItemDelegate>
#include <QStyledItemDelegate>
#include "progressbarpainter.h"
class PreviewListDelegate final : public QItemDelegate
class PreviewListDelegate final : public QStyledItemDelegate
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(PreviewListDelegate)

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -70,16 +71,19 @@ PreviewSelectDialog::PreviewSelectDialog(QWidget *parent, const BitTorrent::Torr
const Preferences *pref = Preferences::instance();
// Preview list
m_previewListModel = new QStandardItemModel(0, NB_COLUMNS, this);
m_previewListModel->setHeaderData(NAME, Qt::Horizontal, tr("Name"));
m_previewListModel->setHeaderData(SIZE, Qt::Horizontal, tr("Size"));
m_previewListModel->setHeaderData(PROGRESS, Qt::Horizontal, tr("Progress"));
auto *previewListModel = new QStandardItemModel(0, NB_COLUMNS, this);
previewListModel->setHeaderData(NAME, Qt::Horizontal, tr("Name"));
previewListModel->setHeaderData(SIZE, Qt::Horizontal, tr("Size"));
previewListModel->setHeaderData(PROGRESS, Qt::Horizontal, tr("Progress"));
m_ui->previewList->setAlternatingRowColors(pref->useAlternatingRowColors());
m_ui->previewList->setModel(m_previewListModel);
m_ui->previewList->setUniformRowHeights(true);
m_ui->previewList->setModel(previewListModel);
m_ui->previewList->hideColumn(FILE_INDEX);
m_listDelegate = new PreviewListDelegate(this);
m_ui->previewList->setItemDelegate(m_listDelegate);
auto *listDelegate = new PreviewListDelegate(this);
m_ui->previewList->setItemDelegate(listDelegate);
// Fill list in
const QVector<qreal> fp = torrent->filesProgress();
for (int i = 0; i < torrent->filesCount(); ++i)
@ -87,20 +91,20 @@ PreviewSelectDialog::PreviewSelectDialog(QWidget *parent, const BitTorrent::Torr
const Path filePath = torrent->filePath(i);
if (Utils::Misc::isPreviewable(filePath))
{
int row = m_previewListModel->rowCount();
m_previewListModel->insertRow(row);
m_previewListModel->setData(m_previewListModel->index(row, NAME), filePath.filename());
m_previewListModel->setData(m_previewListModel->index(row, SIZE), torrent->fileSize(i));
m_previewListModel->setData(m_previewListModel->index(row, PROGRESS), fp[i]);
m_previewListModel->setData(m_previewListModel->index(row, FILE_INDEX), i);
int row = previewListModel->rowCount();
previewListModel->insertRow(row);
previewListModel->setData(previewListModel->index(row, NAME), filePath.filename());
previewListModel->setData(previewListModel->index(row, SIZE), Utils::Misc::friendlyUnit(torrent->fileSize(i)));
previewListModel->setData(previewListModel->index(row, PROGRESS), fp[i]);
previewListModel->setData(previewListModel->index(row, FILE_INDEX), i);
}
}
m_previewListModel->sort(NAME);
previewListModel->sort(NAME);
m_ui->previewList->header()->setContextMenuPolicy(Qt::CustomContextMenu);
m_ui->previewList->header()->setFirstSectionMovable(true);
m_ui->previewList->header()->setSortIndicator(0, Qt::AscendingOrder);
m_ui->previewList->selectionModel()->select(m_previewListModel->index(0, NAME), QItemSelectionModel::Select | QItemSelectionModel::Rows);
m_ui->previewList->selectionModel()->select(previewListModel->index(0, NAME), QItemSelectionModel::Select | QItemSelectionModel::Rows);
connect(m_ui->previewList->header(), &QWidget::customContextMenuRequested, this, &PreviewSelectDialog::displayColumnHeaderMenu);
@ -129,7 +133,7 @@ void PreviewSelectDialog::previewButtonClicked()
// File
if (!path.exists())
{
const bool isSingleFile = (m_previewListModel->rowCount() == 1);
const bool isSingleFile = (m_ui->previewList->model()->rowCount() == 1);
QWidget *parent = isSingleFile ? this->parentWidget() : this;
QMessageBox::critical(parent, tr("Preview impossible")
, tr("Sorry, we can't preview this file: \"%1\".").arg(path.toString()));
@ -199,6 +203,6 @@ void PreviewSelectDialog::showEvent(QShowEvent *event)
}
// Only one file, no choice
if (m_previewListModel->rowCount() <= 1)
if (m_ui->previewList->model()->rowCount() <= 1)
previewButtonClicked();
}

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -33,8 +34,6 @@
#include "base/path.h"
#include "base/settingvalue.h"
class QStandardItemModel;
namespace BitTorrent
{
class Torrent;
@ -44,7 +43,6 @@ namespace Ui
{
class PreviewSelectDialog;
}
class PreviewListDelegate;
class PreviewSelectDialog final : public QDialog
{
@ -79,8 +77,6 @@ private:
void saveWindowState();
Ui::PreviewSelectDialog *m_ui = nullptr;
QStandardItemModel *m_previewListModel = nullptr;
PreviewListDelegate *m_listDelegate = nullptr;
const BitTorrent::Torrent *m_torrent = nullptr;
bool m_headerStateInitialized = false;

View file

@ -82,6 +82,7 @@ PropertiesWidget::PropertiesWidget(QWidget *parent)
m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine);
m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open);
m_ui->filesList->setOpenByEnterKey(true);
// SIGNAL/SLOTS
connect(m_ui->selectAllButton, &QPushButton::clicked, m_ui->filesList, &TorrentContentWidget::checkAll);

View file

@ -192,7 +192,7 @@
</property>
</widget>
</item>
<item row="2" column="3" colspan="4">
<item row="2" column="3">
<widget class="QLabel" name="labelUpSpeedVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@ -382,7 +382,7 @@
</property>
</widget>
</item>
<item row="1" column="3" colspan="4">
<item row="1" column="3">
<widget class="QLabel" name="labelUpTotalVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">

View file

@ -89,10 +89,6 @@ TorrentContentWidget::TorrentContentWidget(QWidget *parent)
const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
connect(renameFileHotkey, &QShortcut::activated, this, &TorrentContentWidget::renameSelectedFile);
const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
connect(openFileHotkeyReturn, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
connect(openFileHotkeyEnter, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
connect(model(), &QAbstractItemModel::modelReset, this, &TorrentContentWidget::expandRecursively);
}
@ -118,6 +114,32 @@ void TorrentContentWidget::refresh()
setUpdatesEnabled(true);
}
bool TorrentContentWidget::openByEnterKey() const
{
return m_openFileHotkeyEnter;
}
void TorrentContentWidget::setOpenByEnterKey(const bool value)
{
if (value == openByEnterKey())
return;
if (value)
{
m_openFileHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
connect(m_openFileHotkeyReturn, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
m_openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
connect(m_openFileHotkeyEnter, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
}
else
{
delete m_openFileHotkeyEnter;
m_openFileHotkeyEnter = nullptr;
delete m_openFileHotkeyReturn;
m_openFileHotkeyReturn = nullptr;
}
}
TorrentContentWidget::DoubleClickAction TorrentContentWidget::doubleClickAction() const
{
return m_doubleClickAction;

View file

@ -34,6 +34,8 @@
#include "base/bittorrent/downloadpriority.h"
#include "base/pathfwd.h"
class QShortcut;
namespace BitTorrent
{
class Torrent;
@ -78,6 +80,9 @@ public:
BitTorrent::TorrentContentHandler *contentHandler() const;
void refresh();
bool openByEnterKey() const;
void setOpenByEnterKey(bool value);
DoubleClickAction doubleClickAction() const;
void setDoubleClickAction(DoubleClickAction action);
@ -118,4 +123,6 @@ private:
TorrentContentFilterModel *m_filterModel;
DoubleClickAction m_doubleClickAction = DoubleClickAction::Rename;
ColumnsVisibilityMode m_columnsVisibilityMode = ColumnsVisibilityMode::Editable;
QShortcut *m_openFileHotkeyEnter = nullptr;
QShortcut *m_openFileHotkeyReturn = nullptr;
};

View file

@ -117,6 +117,7 @@ TransferListModel::TransferListModel(QObject *parent)
, m_completedIcon {UIThemeManager::instance()->getIcon(u"checked-completed"_s, u"completed"_s)}
, m_downloadingIcon {UIThemeManager::instance()->getIcon(u"downloading"_s)}
, m_errorIcon {UIThemeManager::instance()->getIcon(u"error"_s)}
, m_movingIcon {UIThemeManager::instance()->getIcon(u"set-location"_s)}
, m_pausedIcon {UIThemeManager::instance()->getIcon(u"stopped"_s, u"media-playback-pause"_s)}
, m_queuedIcon {UIThemeManager::instance()->getIcon(u"queued"_s)}
, m_stalledDLIcon {UIThemeManager::instance()->getIcon(u"stalledDL"_s)}
@ -710,8 +711,9 @@ QIcon TransferListModel::getIconByState(const BitTorrent::TorrentState state) co
case BitTorrent::TorrentState::CheckingDownloading:
case BitTorrent::TorrentState::CheckingUploading:
case BitTorrent::TorrentState::CheckingResumeData:
case BitTorrent::TorrentState::Moving:
return m_checkingIcon;
case BitTorrent::TorrentState::Moving:
return m_movingIcon;
case BitTorrent::TorrentState::Unknown:
case BitTorrent::TorrentState::MissingFiles:
case BitTorrent::TorrentState::Error:

View file

@ -137,6 +137,7 @@ private:
QIcon m_completedIcon;
QIcon m_downloadingIcon;
QIcon m_errorIcon;
QIcon m_movingIcon;
QIcon m_pausedIcon;
QIcon m_queuedIcon;
QIcon m_stalledDLIcon;

View file

@ -327,6 +327,7 @@
<file>qbittorrent-tray-dark.svg</file>
<file>qbittorrent-tray-light.svg</file>
<file>qbittorrent-tray.svg</file>
<file alias="qbittorrent.svg">qbittorrent-tray.svg</file>
<file>queued.svg</file>
<file>ratio.svg</file>
<file>reannounce.svg</file>

View file

@ -4,7 +4,6 @@ add_library(qbt_webui STATIC
api/apierror.h
api/appcontroller.h
api/authcontroller.h
api/freediskspacechecker.h
api/isessionmanager.h
api/logcontroller.h
api/rsscontroller.h
@ -13,6 +12,7 @@ add_library(qbt_webui STATIC
api/torrentscontroller.h
api/transfercontroller.h
api/serialize/serialize_torrent.h
freediskspacechecker.h
webapplication.h
webui.h
@ -21,7 +21,6 @@ add_library(qbt_webui STATIC
api/apierror.cpp
api/appcontroller.cpp
api/authcontroller.cpp
api/freediskspacechecker.cpp
api/logcontroller.cpp
api/rsscontroller.cpp
api/searchcontroller.cpp
@ -29,6 +28,7 @@ add_library(qbt_webui STATIC
api/torrentscontroller.cpp
api/transfercontroller.cpp
api/serialize/serialize_torrent.cpp
freediskspacechecker.cpp
webapplication.cpp
webui.cpp
)

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, []
{
@ -193,6 +193,16 @@ void AppController::preferencesAction()
data[u"max_uploads"_s] = session->maxUploads();
data[u"max_uploads_per_torrent"_s] = session->maxUploadsPerTorrent();
// I2P
data[u"i2p_enabled"_s] = session->isI2PEnabled();
data[u"i2p_address"_s] = session->I2PAddress();
data[u"i2p_port"_s] = session->I2PPort();
data[u"i2p_mixed_mode"_s] = session->I2PMixedMode();
data[u"i2p_inbound_quantity"_s] = session->I2PInboundQuantity();
data[u"i2p_outbound_quantity"_s] = session->I2POutboundQuantity();
data[u"i2p_inbound_length"_s] = session->I2PInboundLength();
data[u"i2p_outbound_length"_s] = session->I2POutboundLength();
// Proxy Server
const auto *proxyManager = Net::ProxyConfigurationManager::instance();
Net::ProxyConfiguration proxyConf = proxyManager->proxyConfiguration();
@ -265,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();
@ -628,6 +638,24 @@ void AppController::setPreferencesAction()
if (hasKey(u"max_uploads_per_torrent"_s))
session->setMaxUploadsPerTorrent(it.value().toInt());
// I2P
if (hasKey(u"i2p_enabled"_s))
session->setI2PEnabled(it.value().toBool());
if (hasKey(u"i2p_address"_s))
session->setI2PAddress(it.value().toString());
if (hasKey(u"i2p_port"_s))
session->setI2PPort(it.value().toInt());
if (hasKey(u"i2p_mixed_mode"_s))
session->setI2PMixedMode(it.value().toBool());
if (hasKey(u"i2p_inbound_quantity"_s))
session->setI2PInboundQuantity(it.value().toInt());
if (hasKey(u"i2p_outbound_quantity"_s))
session->setI2POutboundQuantity(it.value().toInt());
if (hasKey(u"i2p_inbound_length"_s))
session->setI2PInboundLength(it.value().toInt());
if (hasKey(u"i2p_outbound_length"_s))
session->setI2POutboundLength(it.value().toInt());
// Proxy Server
auto *proxyManager = Net::ProxyConfigurationManager::instance();
Net::ProxyConfiguration proxyConf = proxyManager->proxyConfiguration();
@ -754,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());
@ -790,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())
{
@ -64,12 +74,8 @@ void AuthController::loginAction()
, 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

@ -33,7 +33,6 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QMetaObject>
#include <QThreadPool>
#include "base/algorithm.h"
#include "base/bittorrent/cachestatus.h"
@ -50,13 +49,10 @@
#include "base/preferences.h"
#include "base/utils/string.h"
#include "apierror.h"
#include "freediskspacechecker.h"
#include "serialize/serialize_torrent.h"
namespace
{
const int FREEDISKSPACE_CHECK_TIMEOUT = 30000;
// Sync main data keys
const QString KEY_SYNC_MAINDATA_QUEUEING = u"queueing"_s;
const QString KEY_SYNC_MAINDATA_REFRESH_INTERVAL = u"refresh_interval"_s;
@ -391,8 +387,11 @@ namespace
SyncController::SyncController(IApplication *app, QObject *parent)
: APIController(app, parent)
{
invokeChecker();
m_freeDiskSpaceElapsedTimer.start();
}
void SyncController::updateFreeDiskSpace(const qint64 freeDiskSpace)
{
m_freeDiskSpace = freeDiskSpace;
}
// The function returns the changed data from the server to synchronize with the web client.
@ -552,7 +551,7 @@ void SyncController::makeMaindataSnapshot()
}
m_maindataSnapshot.serverState = getTransferInfo();
m_maindataSnapshot.serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace();
m_maindataSnapshot.serverState[KEY_TRANSFER_FREESPACEONDISK] = m_freeDiskSpace;
m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled();
m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled();
m_maindataSnapshot.serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval();
@ -661,7 +660,7 @@ QJsonObject SyncController::generateMaindataSyncData(const int id, const bool fu
m_removedTrackers.clear();
QVariantMap serverState = getTransferInfo();
serverState[KEY_TRANSFER_FREESPACEONDISK] = getFreeDiskSpace();
serverState[KEY_TRANSFER_FREESPACEONDISK] = m_freeDiskSpace;
serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled();
serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled();
serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval();
@ -782,34 +781,6 @@ void SyncController::torrentPeersAction()
setResult(generateSyncData(acceptedResponseId, data, m_lastAcceptedPeersResponse, m_lastPeersResponse));
}
qint64 SyncController::getFreeDiskSpace()
{
if (m_freeDiskSpaceElapsedTimer.hasExpired(FREEDISKSPACE_CHECK_TIMEOUT))
invokeChecker();
return m_freeDiskSpace;
}
void SyncController::invokeChecker()
{
if (m_isFreeDiskSpaceCheckerRunning)
return;
auto *freeDiskSpaceChecker = new FreeDiskSpaceChecker;
connect(freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, this, [this](const qint64 freeSpaceSize)
{
m_freeDiskSpace = freeSpaceSize;
m_isFreeDiskSpaceCheckerRunning = false;
m_freeDiskSpaceElapsedTimer.restart();
});
connect(freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, freeDiskSpaceChecker, &QObject::deleteLater);
m_isFreeDiskSpaceCheckerRunning = true;
QThreadPool::globalInstance()->start([freeDiskSpaceChecker]
{
freeDiskSpaceChecker->check();
});
}
void SyncController::onCategoryAdded(const QString &categoryName)
{
m_removedCategories.remove(categoryName);

View file

@ -28,22 +28,17 @@
#pragma once
#include <QElapsedTimer>
#include <QVariantMap>
#include <QSet>
#include <QVariantMap>
#include "base/bittorrent/infohash.h"
#include "apicontroller.h"
class QThread;
namespace BitTorrent
{
class Torrent;
}
class FreeDiskSpaceChecker;
class SyncController : public APIController
{
Q_OBJECT
@ -54,14 +49,14 @@ public:
explicit SyncController(IApplication *app, QObject *parent = nullptr);
public slots:
void updateFreeDiskSpace(qint64 freeDiskSpace);
private slots:
void maindataAction();
void torrentPeersAction();
private:
qint64 getFreeDiskSpace();
void invokeChecker();
void makeMaindataSnapshot();
QJsonObject generateMaindataSyncData(int id, bool fullUpdate);
@ -85,8 +80,6 @@ private:
void onTorrentTrackersChanged(BitTorrent::Torrent *torrent);
qint64 m_freeDiskSpace = 0;
QElapsedTimer m_freeDiskSpaceElapsedTimer;
bool m_isFreeDiskSpaceCheckerRunning = false;
QVariantMap m_lastPeersResponse;
QVariantMap m_lastAcceptedPeersResponse;

View file

@ -232,6 +232,11 @@ namespace
}
}
void TorrentsController::countAction()
{
setResult(QString::number(BitTorrent::Session::instance()->torrentsCount()));
}
// Returns all the torrents in JSON format.
// The return value is a JSON-formatted list of dictionaries.
// The dictionary keys are:

View file

@ -39,6 +39,7 @@ public:
using APIController::APIController;
private slots:
void countAction();
void infoAction();
void propertiesAction();
void trackersAction();

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
*
* This program is free software; you can redistribute it and/or
@ -31,8 +32,13 @@
#include "base/bittorrent/session.h"
#include "base/utils/fs.h"
qint64 FreeDiskSpaceChecker::lastResult() const
{
return m_lastResult;
}
void FreeDiskSpaceChecker::check()
{
const qint64 freeDiskSpace = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->savePath());
emit checked(freeDiskSpace);
m_lastResult = Utils::Fs::freeDiskSpaceOnPath(BitTorrent::Session::instance()->savePath());
emit checked(m_lastResult);
}

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
*
* This program is free software; you can redistribute it and/or
@ -38,9 +39,14 @@ class FreeDiskSpaceChecker final : public QObject
public:
using QObject::QObject;
qint64 lastResult() const;
public slots:
void check();
signals:
void checked(qint64 freeSpaceSize);
private:
qint64 m_lastResult = 0;
};

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014, 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2014, 2022-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
@ -29,16 +29,20 @@
#include "webapplication.h"
#include <algorithm>
#include <chrono>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QJsonDocument>
#include <QMetaObject>
#include <QMimeDatabase>
#include <QMimeType>
#include <QNetworkCookie>
#include <QRegularExpression>
#include <QThread>
#include <QTimer>
#include <QUrl>
#include "base/algorithm.h"
@ -60,6 +64,7 @@
#include "api/synccontroller.h"
#include "api/torrentscontroller.h"
#include "api/transfercontroller.h"
#include "freediskspacechecker.h"
const int MAX_ALLOWED_FILESIZE = 10 * 1024 * 1024;
const QString DEFAULT_SESSION_COOKIE_NAME = u"SID"_s;
@ -68,6 +73,10 @@ const QString WWW_FOLDER = u":/www"_s;
const QString PUBLIC_FOLDER = u"/public"_s;
const QString PRIVATE_FOLDER = u"/private"_s;
using namespace std::chrono_literals;
const std::chrono::seconds FREEDISKSPACE_CHECK_TIMEOUT = 30s;
namespace
{
QStringMap parseCookie(const QStringView cookieStr)
@ -147,6 +156,9 @@ WebApplication::WebApplication(IApplication *app, QObject *parent)
, ApplicationComponent(app)
, m_cacheID {QString::number(Utils::Random::rand(), 36)}
, m_authController {new AuthController(this, app, this)}
, m_workerThread {new QThread}
, m_freeDiskSpaceChecker {new FreeDiskSpaceChecker}
, m_freeDiskSpaceCheckingTimer {new QTimer(this)}
{
declarePublicAPI(u"auth/login"_s);
@ -163,6 +175,16 @@ WebApplication::WebApplication(IApplication *app, QObject *parent)
}
m_sessionCookieName = DEFAULT_SESSION_COOKIE_NAME;
}
m_freeDiskSpaceChecker->moveToThread(m_workerThread.get());
connect(m_workerThread.get(), &QThread::finished, m_freeDiskSpaceChecker, &QObject::deleteLater);
m_workerThread->start();
m_freeDiskSpaceCheckingTimer->setInterval(FREEDISKSPACE_CHECK_TIMEOUT);
m_freeDiskSpaceCheckingTimer->setSingleShot(true);
connect(m_freeDiskSpaceCheckingTimer, &QTimer::timeout, m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::check);
connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, m_freeDiskSpaceCheckingTimer, qOverload<>(&QTimer::start));
QMetaObject::invokeMethod(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::check);
}
WebApplication::~WebApplication()
@ -269,6 +291,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 +389,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 +411,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 +443,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});
@ -680,14 +712,18 @@ void WebApplication::sessionStart()
});
m_currentSession = new WebSession(generateSid(), app());
m_sessions[m_currentSession->id()] = m_currentSession;
m_currentSession->registerAPIController<AppController>(u"app"_s);
m_currentSession->registerAPIController<LogController>(u"log"_s);
m_currentSession->registerAPIController<RSSController>(u"rss"_s);
m_currentSession->registerAPIController<SearchController>(u"search"_s);
m_currentSession->registerAPIController<SyncController>(u"sync"_s);
m_currentSession->registerAPIController<TorrentsController>(u"torrents"_s);
m_currentSession->registerAPIController<TransferController>(u"transfer"_s);
m_sessions[m_currentSession->id()] = m_currentSession;
auto *syncController = m_currentSession->registerAPIController<SyncController>(u"sync"_s);
syncController->updateFreeDiskSpace(m_freeDiskSpaceChecker->lastResult());
connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, syncController, &SyncController::updateFreeDiskSpace);
QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toUtf8()};
cookie.setHttpOnly(true);

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014, 2017, 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2014, 2017, 2022-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
@ -49,13 +49,17 @@
#include "base/http/types.h"
#include "base/path.h"
#include "base/utils/net.h"
#include "base/utils/thread.h"
#include "base/utils/version.h"
#include "api/isessionmanager.h"
inline const Utils::Version<3, 2> API_VERSION {2, 9, 2};
inline const Utils::Version<3, 2> API_VERSION {2, 9, 3};
class QTimer;
class APIController;
class AuthController;
class FreeDiskSpaceChecker;
class WebApplication;
class WebSession final : public QObject, public ApplicationComponent, public ISession
@ -69,10 +73,12 @@ public:
void updateTimestamp();
template <typename T>
void registerAPIController(const QString &scope)
T *registerAPIController(const QString &scope)
{
static_assert(std::is_base_of_v<APIController, T>, "Class should be derived from APIController.");
m_apiControllers[scope] = new T(app(), this);
auto *controller = new T(app(), this);
m_apiControllers[scope] = controller;
return controller;
}
APIController *getAPIController(const QString &scope) const;
@ -97,15 +103,18 @@ public:
Http::Response processRequest(const Http::Request &request, const Http::Environment &env) override;
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;
void sessionStart() override;
void sessionEnd() override;
const Http::Request &request() const;
const Http::Environment &env() const;
private:
void doProcessRequest();
void configure();
@ -241,4 +250,8 @@ private:
QHostAddress m_clientAddress;
QVector<Http::Header> m_prebuiltHeaders;
Utils::Thread::UniquePtr m_workerThread;
FreeDiskSpaceChecker *m_freeDiskSpaceChecker = nullptr;
QTimer *m_freeDiskSpaceCheckingTimer = nullptr;
};

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;
};

View file

@ -3,7 +3,6 @@ HEADERS += \
$$PWD/api/apierror.h \
$$PWD/api/appcontroller.h \
$$PWD/api/authcontroller.h \
$$PWD/api/freediskspacechecker.h \
$$PWD/api/isessionmanager.h \
$$PWD/api/logcontroller.h \
$$PWD/api/rsscontroller.h \
@ -12,6 +11,7 @@ HEADERS += \
$$PWD/api/torrentscontroller.h \
$$PWD/api/transfercontroller.h \
$$PWD/api/serialize/serialize_torrent.h \
$$PWD/freediskspacechecker.h \
$$PWD/webapplication.h \
$$PWD/webui.h
@ -20,7 +20,6 @@ SOURCES += \
$$PWD/api/apierror.cpp \
$$PWD/api/appcontroller.cpp \
$$PWD/api/authcontroller.cpp \
$$PWD/api/freediskspacechecker.cpp \
$$PWD/api/logcontroller.cpp \
$$PWD/api/rsscontroller.cpp \
$$PWD/api/searchcontroller.cpp \
@ -28,6 +27,7 @@ SOURCES += \
$$PWD/api/torrentscontroller.cpp \
$$PWD/api/transfercontroller.cpp \
$$PWD/api/serialize/serialize_torrent.cpp \
$$PWD/freediskspacechecker.cpp \
$$PWD/webapplication.cpp \
$$PWD/webui.cpp

View file

@ -178,8 +178,7 @@ a.propButton img {
}
.scrollableMenu {
overflow-x: hidden;
overflow-y: auto;
overflow: hidden auto;
}
/* context menu specific */

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Renaming))QBT_TR[CONTEXT=TorrentContentTreeView]</title>
<title>QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]</title>
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/filesystem.js?v=${CACHEID}"></script>

View file

@ -488,7 +488,21 @@ window.addEvent('load', function() {
Object.each(category_list, function(category) {
sortedCategories.push(category.name);
});
sortedCategories.sort();
sortedCategories.sort(function(category1, category2) {
for (let i = 0; i < Math.min(category1.length, category2.length); ++i) {
if (category1[i] === "/" && category2[i] !== "/") {
return -1;
}
else if (category1[i] !== "/" && category2[i] === "/") {
return 1;
}
else if (category1[i] !== category2[i]) {
return category1[i].localeCompare(category2[i]);
}
}
return category1.length - category2.length;
});
for (let i = 0; i < sortedCategories.length; ++i) {
const categoryName = sortedCategories[i];

View file

@ -111,7 +111,8 @@ window.qBittorrent.DynamicTable = (function() {
let n = 2;
while (panel.clientWidth != panel.offsetWidth && n > 0) { // is panel vertical scrollbar visible ?
// is panel vertical scrollbar visible or does panel content not fit?
while (((panel.clientWidth != panel.offsetWidth) || (panel.clientHeight != panel.scrollHeight)) && (n > 0)) {
--n;
h -= 0.5;
$(this.dynamicTableDivId).style.height = h + 'px';
@ -1001,10 +1002,13 @@ window.qBittorrent.DynamicTable = (function() {
case "checkingUP":
case "queuedForChecking":
case "checkingResumeData":
case "moving":
state = "force-recheck";
img_path = "images/force-recheck.svg";
break;
case "moving":
state = "moving";
img_path = "images/set-location.svg";
break;
case "error":
case "unknown":
case "missingFiles":

View file

@ -167,7 +167,7 @@
window.parent.closeWindows();
});
if (Browser.platform === 'ios') {
if ((Browser.platform === 'ios') || ((Browser.platform === 'mac') && (navigator.maxTouchPoints > 1))) {
$('fileselect').accept = ".torrent";
}
</script>

View file

@ -349,6 +349,33 @@
</table>
</fieldset>
<fieldset class="settings">
<legend>
<input type="checkbox" id="i2pEnabledCheckbox" onclick="qBittorrent.Preferences.updateI2PSettingsEnabled();" />
<label for="i2pEnabledCheckbox">QBT_TR(I2P (Experimental) (requires libtorrent &gt;= 2.0))QBT_TR[CONTEXT=OptionsDialog]</label>
</legend>
<table>
<tr>
<td>
<label for="i2pAddress">QBT_TR(Host:)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
<td>
<input type="text" id="i2pAddress" />
</td>
<td>
<label for="i2pPort">QBT_TR(Port:)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
<td>
<input type="number" id="i2pPort" min="0" max="65535" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 5em;" />
</td>
</tr>
</table>
<div class="formRow">
<input type="checkbox" id="i2pMixedMode" title="QBT_TR(If &quot;mixed mode&quot; is enabled, I2P torrents are allowed to also get peers from other sources than the tracker, and connect to regular IPs, not providing any anonymization. This may be useful if the user is not interested in the anonymization of I2P, but still wants to be able to connect to I2P peers.)QBT_TR[CONTEXT=OptionsDialog]" />
<label for="i2pMixedMode">QBT_TR(Mixed mode)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
</fieldset>
<fieldset class="settings">
<legend>QBT_TR(Proxy Server)QBT_TR[CONTEXT=OptionsDialog]</legend>
<table>
@ -1420,6 +1447,38 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
<input type="text" id="requestQueueSize" style="width: 15em;" />
</td>
</tr>
<tr>
<td>
<label for="i2pInboundQuantity">QBT_TR(I2P inbound quantity (requires libtorrent &gt;= 2.0):)QBT_TR[CONTEXT=OptionsDialog]&nbsp;<a href="https://www.libtorrent.org/reference-Settings.html#i2p_inbound_quantity" target="_blank">(?)</a></label>
</td>
<td>
<input id="i2pInboundQuantity" type="number" min="1" max="16" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 15em;" />
</td>
</tr>
<tr>
<td>
<label for="i2pOutboundQuantity">QBT_TR(I2P outbound quantity (requires libtorrent &gt;= 2.0):)QBT_TR[CONTEXT=OptionsDialog]&nbsp;<a href="https://www.libtorrent.org/reference-Settings.html#i2p_outbound_quantity" target="_blank">(?)</a></label>
</td>
<td>
<input id="i2pOutboundQuantity" type="number" min="1" max="16" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 15em;" />
</td>
</tr>
<tr>
<td>
<label for="i2pInboundLength">QBT_TR(I2P inbound length (requires libtorrent &gt;= 2.0):)QBT_TR[CONTEXT=OptionsDialog]&nbsp;<a href="https://www.libtorrent.org/reference-Settings.html#i2p_inbound_length" target="_blank">(?)</a></label>
</td>
<td>
<input id="i2pInboundLength" type="number" min="0" max="7" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 15em;" />
</td>
</tr>
<tr>
<td>
<label for="i2pOutboundLength">QBT_TR(I2P outbound length (requires libtorrent &gt;= 2.0):)QBT_TR[CONTEXT=OptionsDialog]&nbsp;<a href="https://www.libtorrent.org/reference-Settings.html#i2p_outbound_length" target="_blank">(?)</a></label>
</td>
<td>
<input id="i2pOutboundLength" type="number" min="0" max="7" onchange="qBittorrent.Preferences.numberInputLimiter(this);" style="width: 15em;" />
</td>
</tr>
</table>
</fieldset>
</div>
@ -1455,6 +1514,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
updateMaxConnecPerTorrentEnabled: updateMaxConnecPerTorrentEnabled,
updateMaxUploadsEnabled: updateMaxUploadsEnabled,
updateMaxUploadsPerTorrentEnabled: updateMaxUploadsPerTorrentEnabled,
updateI2PSettingsEnabled: updateI2PSettingsEnabled,
updatePeerProxySettings: updatePeerProxySettings,
updatePeerProxyAuthSettings: updatePeerProxyAuthSettings,
updateFilterSettings: updateFilterSettings,
@ -1644,6 +1704,13 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
$('max_uploads_per_torrent_value').setProperty('disabled', !isMaxUploadsPerTorrentEnabled);
};
const updateI2PSettingsEnabled = function() {
const isI2PEnabled = $('i2pEnabledCheckbox').getProperty('checked');
$('i2pAddress').setProperty('disabled', !isI2PEnabled);
$('i2pPort').setProperty('disabled', !isI2PEnabled);
$('i2pMixedMode').setProperty('disabled', !isI2PEnabled);
};
const updatePeerProxySettings = function() {
const proxyType = $('peer_proxy_type_select').getProperty('value');
const isProxyDisabled = (proxyType === "None");
@ -2023,6 +2090,13 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
}
updateMaxUploadsPerTorrentEnabled();
// I2P
$('i2pEnabledCheckbox').setProperty('checked', pref.i2p_enabled);
$('i2pAddress').setProperty('value', pref.i2p_address);
$('i2pPort').setProperty('value', pref.i2p_port);
$('i2pMixedMode').setProperty('checked', pref.i2p_mixed_mode);
updateI2PSettingsEnabled();
// Proxy Server
$('peer_proxy_type_select').setProperty('value', pref.proxy_type);
$('peer_proxy_host_text').setProperty('value', pref.proxy_ip);
@ -2241,6 +2315,10 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
$('peerTurnoverCutoff').setProperty('value', pref.peer_turnover_cutoff);
$('peerTurnoverInterval').setProperty('value', pref.peer_turnover_interval);
$('requestQueueSize').setProperty('value', pref.request_queue_size);
$('i2pInboundQuantity').setProperty('value', pref.i2p_inbound_quantity);
$('i2pOutboundQuantity').setProperty('value', pref.i2p_outbound_quantity);
$('i2pInboundLength').setProperty('value', pref.i2p_inbound_length);
$('i2pOutboundLength').setProperty('value', pref.i2p_outbound_length);
}
}
}).send();
@ -2360,6 +2438,12 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
}
settings.set('max_uploads_per_torrent', max_uploads_per_torrent);
// I2P
settings.set('i2p_enabled', $('i2pEnabledCheckbox').getProperty('checked'));
settings.set('i2p_address', $('i2pAddress').getProperty('value'));
settings.set('i2p_port', $('i2pPort').getProperty('value').toInt());
settings.set('i2p_mixed_mode', $('i2pMixedMode').getProperty('checked'));
// Proxy Server
settings.set('proxy_type', $('peer_proxy_type_select').getProperty('value'));
settings.set('proxy_ip', $('peer_proxy_host_text').getProperty('value'));
@ -2673,6 +2757,10 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
settings.set('peer_turnover_cutoff', $('peerTurnoverCutoff').getProperty('value'));
settings.set('peer_turnover_interval', $('peerTurnoverInterval').getProperty('value'));
settings.set('request_queue_size', $('requestQueueSize').getProperty('value'));
settings.set('i2p_inbound_quantity', $('i2pInboundQuantity').getProperty('value'));
settings.set('i2p_outbound_quantity', $('i2pOutboundQuantity').getProperty('value'));
settings.set('i2p_inbound_length', $('i2pInboundLength').getProperty('value'));
settings.set('i2p_outbound_length', $('i2pOutboundLength').getProperty('value'));
// Send it to qBT
const json_str = JSON.encode(settings);

View file

@ -593,7 +593,10 @@ Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also
rulesList[rule].torrentParams.category = $('assignCategoryCombobox').value;
rulesList[rule].torrentParams.tags = $('ruleAddTags').value.split(',');
rulesList[rule].torrentParams.save_path = $('savetoDifferentDir').checked ? $('saveToText').value : '';
if ($('savetoDifferentDir').checked) {
rulesList[rule].torrentParams.save_path = $('saveToText').value;
rulesList[rule].torrentParams.use_auto_tmm = false;
}
switch ($('addPausedCombobox').value) {
case 'default':
@ -715,8 +718,6 @@ Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also
$('assignCategoryCombobox').disabled = false;
$('ruleAddTags').disabled = false;
$('savetoDifferentDir').disabled = false;
$('savetoDifferentDir').checked = rulesList[ruleName].torrentParams.save_path ? false : true;
$('saveToText').disabled = rulesList[ruleName].torrentParams.save_path ? false : true;
$('ignoreDaysValue').disabled = false;
$('addPausedCombobox').disabled = false;
$('contentLayoutCombobox').disabled = false;
@ -731,6 +732,7 @@ Supports the formats: S01E01, 1x1, 2017.12.31 and 31.12.2017 (Date formats also
$('assignCategoryCombobox').value = rulesList[ruleName].torrentParams.category ? rulesList[ruleName].torrentParams.category : 'default';
$('ruleAddTags').value = rulesList[ruleName].torrentParams.tags.join(',');
$('savetoDifferentDir').checked = rulesList[ruleName].torrentParams.save_path !== '';
$('saveToText').disabled = !$('savetoDifferentDir').checked;
$('saveToText').value = rulesList[ruleName].torrentParams.save_path;
$('ignoreDaysValue').value = rulesList[ruleName].ignoreDays;

View file

@ -23,14 +23,14 @@
<img src="images/qbittorrent-tray.svg" alt="qBittorrent logo" />
</div>
<div id="formplace" class="col">
<form id="loginform" method="post" onsubmit="submitLoginForm();">
<form id="loginform" method="post" onsubmit="submitLoginForm(event);">
<div class="row">
<label for="username">QBT_TR(Username)QBT_TR[CONTEXT=HttpServer]</label><br />
<input type="text" id="username" name="username" autocomplete="username" />
<input type="text" id="username" name="username" autocomplete="username" required />
</div>
<div class="row">
<label for="password">QBT_TR(Password)QBT_TR[CONTEXT=HttpServer]</label><br />
<input type="password" id="password" name="password" autocomplete="current-password" />
<input type="password" id="password" name="password" autocomplete="current-password" required />
</div>
<div class="row">
<input type="submit" id="login" value="QBT_TR(Login)QBT_TR[CONTEXT=HttpServer]" />

View file

@ -31,13 +31,10 @@
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('username').focus();
document.getElementById('username').select();
document.getElementById('loginform').addEventListener('submit', function(e) {
e.preventDefault();
});
});
function submitLoginForm() {
function submitLoginForm(event) {
event.preventDefault();
const errorMsgElement = document.getElementById('error_msg');
const xhr = new XMLHttpRequest();