diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index fce855ac3..e1d3c7e2e 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -598,6 +598,16 @@ void Preferences::setWebUIPassword(const QByteArray &password) setValue("Preferences/WebUI/Password_PBKDF2", password); } +int Preferences::getWebUISessionTimeout() const +{ + return value("Preferences/WebUI/SessionTimeout", 3600).toInt(); +} + +void Preferences::setWebUISessionTimeout(const int timeout) +{ + setValue("Preferences/WebUI/SessionTimeout", timeout); +} + bool Preferences::isWebUiClickjackingProtectionEnabled() const { return value("Preferences/WebUI/ClickjackingProtection", true).toBool(); diff --git a/src/base/preferences.h b/src/base/preferences.h index 9252a0bdb..480474e9d 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -190,6 +190,8 @@ public: void setWebUiUsername(const QString &username); QByteArray getWebUIPassword() const; void setWebUIPassword(const QByteArray &password); + int getWebUISessionTimeout() const; + void setWebUISessionTimeout(int timeout); // WebUI security bool isWebUiClickjackingProtectionEnabled() const; diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index ee1125d4a..952646dbd 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -403,6 +403,7 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, m_ui->IPSubnetWhitelistButton, &QPushButton::setEnabled); + connect(m_ui->spinSessionTimeout, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkClickjacking, &QCheckBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkCSRFProtection, &QCheckBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->groupHostHeaderValidation, &QGroupBox::toggled, this, &ThisType::enableApplyButton); @@ -724,6 +725,7 @@ void OptionsDialog::saveOptions() pref->setWebUiHttpsEnabled(m_ui->checkWebUiHttps->isChecked()); pref->setWebUIHttpsCertificatePath(m_ui->textWebUIHttpsCert->selectedPath()); pref->setWebUIHttpsKeyPath(m_ui->textWebUIHttpsKey->selectedPath()); + pref->setWebUISessionTimeout(m_ui->spinSessionTimeout->value()); // Authentication pref->setWebUiUsername(webUiUsername()); if (!webUiPassword().isEmpty()) @@ -1092,6 +1094,7 @@ void OptionsDialog::loadOptions() m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUiLocalAuthEnabled()); m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUiAuthSubnetWhitelistEnabled()); m_ui->IPSubnetWhitelistButton->setEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked()); + m_ui->spinSessionTimeout->setValue(pref->getWebUISessionTimeout()); // Security m_ui->checkClickjacking->setChecked(pref->isWebUiClickjackingProtectionEnabled()); diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 433764a15..ceed67c58 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -2922,8 +2922,53 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv Authentication - - + + + + + + + Username: + + + + + + + + + + Password: + + + + + + + QLineEdit::Password + + + Change current password + + + + + + + + + Bypass authentication for clients on localhost + + + + + + + Bypass authentication for clients in whitelisted IP subnets + + + + @@ -2936,46 +2981,42 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv - - - - QLineEdit::Password - - - Change current password - - - - - - - Bypass authentication for clients on localhost - - - - - - - Bypass authentication for clients in whitelisted IP subnets - - - - - - - Username: - - - - - - - Password: - - - - - + + + + + + Session timeout: + + + + + + + Disabled + + + sec + + + 2147483647 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index bbaf36c6b..aa465ea83 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -229,6 +229,7 @@ void AppController::preferencesAction() for (const Utils::Net::Subnet &subnet : asConst(pref->getWebUiAuthSubnetWhitelist())) authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet); data["bypass_auth_subnet_whitelist"] = authSubnetWhitelistStringList.join("\n"); + data["web_ui_session_timeout"] = pref->getWebUISessionTimeout(); // Use alternative Web UI data["alternative_webui_enabled"] = pref->isAltWebUiEnabled(); data["alternative_webui_path"] = pref->getWebUiRootFolder(); @@ -538,6 +539,8 @@ void AppController::setPreferencesAction() // recognize new lines and commas as delimiters pref->setWebUiAuthSubnetWhitelist(it.value().toString().split(QRegularExpression("\n|,"), QString::SkipEmptyParts)); } + if (hasKey("web_ui_session_timeout")) + pref->setWebUISessionTimeout(it.value().toInt()); // Use alternative Web UI if (hasKey("alternative_webui_enabled")) pref->setAltWebUiEnabled(it.value().toBool()); diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index 07bbb68ec..8a38318e9 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -332,6 +332,7 @@ void WebApplication::configure() m_isLocalAuthEnabled = pref->isWebUiLocalAuthEnabled(); m_isAuthSubnetWhitelistEnabled = pref->isWebUiAuthSubnetWhitelistEnabled(); m_authSubnetWhitelist = pref->getWebUiAuthSubnetWhitelist(); + m_sessionTimeout = pref->getWebUISessionTimeout(); m_domainList = pref->getServerDomains().split(';', QString::SkipEmptyParts); std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); }); @@ -471,8 +472,7 @@ void WebApplication::sessionInitialize() if (!sessionId.isEmpty()) { m_currentSession = m_sessions.value(sessionId); if (m_currentSession) { - const qint64 now = QDateTime::currentMSecsSinceEpoch() / 1000; - if ((now - m_currentSession->m_timestamp) > INACTIVE_TIME) { + if (m_currentSession->hasExpired(m_sessionTimeout)) { // session is outdated - removing it delete m_sessions.take(sessionId); m_currentSession = nullptr; @@ -523,14 +523,14 @@ void WebApplication::sessionStart() Q_ASSERT(!m_currentSession); // remove outdated sessions - const qint64 now = QDateTime::currentMSecsSinceEpoch() / 1000; - Algorithm::removeIf(m_sessions, [now](const QString &, const WebSession *session) + Algorithm::removeIf(m_sessions, [this](const QString &, const WebSession *session) { - if ((now - session->timestamp()) <= INACTIVE_TIME) - return false; + if (session->hasExpired(m_sessionTimeout)) { + delete session; + return true; + } - delete session; - return true; + return false; }); m_currentSession = new WebSession(generateSid()); @@ -650,9 +650,16 @@ QString WebSession::id() const return m_sid; } -qint64 WebSession::timestamp() const +bool WebSession::hasExpired(const qint64 seconds) const { - return m_timestamp; + if (seconds <= 0) + return false; + return m_timer.hasExpired(seconds * 1000); +} + +void WebSession::updateTimestamp() +{ + m_timer.start(); } QVariant WebSession::getData(const QString &id) const @@ -664,8 +671,3 @@ void WebSession::setData(const QString &id, const QVariant &data) { m_data[id] = data; } - -void WebSession::updateTimestamp() -{ - m_timestamp = QDateTime::currentMSecsSinceEpoch() / 1000; -} diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index e59e0d97f..c6fb53811 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -29,6 +29,7 @@ #pragma once #include +#include #include #include #include @@ -48,26 +49,23 @@ class APIController; class WebApplication; constexpr char C_SID[] = "SID"; // name of session id cookie -constexpr int INACTIVE_TIME = 900; // Session inactive time (in secs = 15 min.) class WebSession : public ISession { - friend class WebApplication; - public: explicit WebSession(const QString &sid); QString id() const override; - qint64 timestamp() const; + + bool hasExpired(qint64 seconds) const; + void updateTimestamp(); QVariant getData(const QString &id) const override; void setData(const QString &id, const QVariant &data) override; private: - void updateTimestamp(); - const QString m_sid; - qint64 m_timestamp; + QElapsedTimer m_timer; // timestamp QVariantHash m_data; }; @@ -148,6 +146,7 @@ private: bool m_isLocalAuthEnabled; bool m_isAuthSubnetWhitelistEnabled; QList m_authSubnetWhitelist; + int m_sessionTimeout; // security related QStringList m_domainList; diff --git a/src/webui/www/private/preferences_content.html b/src/webui/www/private/preferences_content.html index 69b380061..a0ef0bc5b 100644 --- a/src/webui/www/private/preferences_content.html +++ b/src/webui/www/private/preferences_content.html @@ -731,6 +731,12 @@
+ + + + + +
  QBT_TR(sec)QBT_TR[CONTEXT=OptionsDialog]
@@ -1340,6 +1346,7 @@ $('bypass_auth_subnet_whitelist_checkbox').setProperty('checked', pref.bypass_auth_subnet_whitelist_enabled); $('bypass_auth_subnet_whitelist_textarea').setProperty('value', pref.bypass_auth_subnet_whitelist); updateBypasssAuthSettings(); + $('webUISessionTimeoutInput').setProperty('value', pref.web_ui_session_timeout.toInt()); // Use alternative Web UI $('use_alt_webui_checkbox').setProperty('checked', pref.alternative_webui_enabled); @@ -1667,6 +1674,7 @@ settings.set('bypass_local_auth', $('bypass_local_auth_checkbox').getProperty('checked')); settings.set('bypass_auth_subnet_whitelist_enabled', $('bypass_auth_subnet_whitelist_checkbox').getProperty('checked')); settings.set('bypass_auth_subnet_whitelist', $('bypass_auth_subnet_whitelist_textarea').getProperty('value')); + settings.set('web_ui_session_timeout', $('webUISessionTimeoutInput').getProperty('value')); // Use alternative Web UI var alternative_webui_enabled = $('use_alt_webui_checkbox').getProperty('checked');