mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-17 06:01:33 -06:00
Use Permanent Cookie
Previously, WebUI was using a HTTP Session Cookie. This type of cookie is tend to be dropped by the browser on mobile platforms and gives a bad experience on the WebUI. Now the cookie is a permanent one and is guaranteed to be persisted between browser restarts. Closes #20993. PR #23392.
This commit is contained in:
@@ -51,6 +51,7 @@ namespace Http
|
|||||||
inline const QString HEADER_CONTENT_LENGTH = u"content-length"_s;
|
inline const QString HEADER_CONTENT_LENGTH = u"content-length"_s;
|
||||||
inline const QString HEADER_CONTENT_SECURITY_POLICY = u"content-security-policy"_s;
|
inline const QString HEADER_CONTENT_SECURITY_POLICY = u"content-security-policy"_s;
|
||||||
inline const QString HEADER_CONTENT_TYPE = u"content-type"_s;
|
inline const QString HEADER_CONTENT_TYPE = u"content-type"_s;
|
||||||
|
inline const QString HEADER_COOKIE = u"cookie"_s;
|
||||||
inline const QString HEADER_CROSS_ORIGIN_OPENER_POLICY = u"cross-origin-opener-policy"_s;
|
inline const QString HEADER_CROSS_ORIGIN_OPENER_POLICY = u"cross-origin-opener-policy"_s;
|
||||||
inline const QString HEADER_DATE = u"date"_s;
|
inline const QString HEADER_DATE = u"date"_s;
|
||||||
inline const QString HEADER_HOST = u"host"_s;
|
inline const QString HEADER_HOST = u"host"_s;
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
#include "webapplication.h"
|
#include "webapplication.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
@@ -88,7 +87,7 @@ namespace
|
|||||||
QStringMap ret;
|
QStringMap ret;
|
||||||
const QList<QStringView> cookies = cookieStr.split(u';', Qt::SkipEmptyParts);
|
const QList<QStringView> cookies = cookieStr.split(u';', Qt::SkipEmptyParts);
|
||||||
|
|
||||||
for (const auto &cookie : cookies)
|
for (const QStringView cookie : cookies)
|
||||||
{
|
{
|
||||||
const qsizetype idx = cookie.indexOf(u'=');
|
const qsizetype idx = cookie.indexOf(u'=');
|
||||||
if (idx < 0)
|
if (idx < 0)
|
||||||
@@ -462,9 +461,13 @@ void WebApplication::configure()
|
|||||||
m_isLocalAuthEnabled = pref->isWebUILocalAuthEnabled();
|
m_isLocalAuthEnabled = pref->isWebUILocalAuthEnabled();
|
||||||
m_isAuthSubnetWhitelistEnabled = pref->isWebUIAuthSubnetWhitelistEnabled();
|
m_isAuthSubnetWhitelistEnabled = pref->isWebUIAuthSubnetWhitelistEnabled();
|
||||||
m_authSubnetWhitelist = pref->getWebUIAuthSubnetWhitelist();
|
m_authSubnetWhitelist = pref->getWebUIAuthSubnetWhitelist();
|
||||||
m_sessionTimeout = pref->getWebUISessionTimeout();
|
m_sessionTimeout = std::chrono::seconds(pref->getWebUISessionTimeout());
|
||||||
m_sessionCookieName = SESSION_COOKIE_NAME_PREFIX + QString::number(pref->getWebUIPort());
|
m_sessionCookieName = SESSION_COOKIE_NAME_PREFIX + QString::number(pref->getWebUIPort());
|
||||||
|
|
||||||
|
// all sessions need to update the cookie expiration date
|
||||||
|
for (WebSession *session : asConst(m_sessions))
|
||||||
|
session->setCookieRefreshTime(0s);
|
||||||
|
|
||||||
m_domainList = pref->getServerDomains().split(u';', Qt::SkipEmptyParts);
|
m_domainList = pref->getServerDomains().split(u';', Qt::SkipEmptyParts);
|
||||||
for (QString &entry : m_domainList)
|
for (QString &entry : m_domainList)
|
||||||
entry = entry.trimmed();
|
entry = entry.trimmed();
|
||||||
@@ -682,7 +685,7 @@ void WebApplication::sessionInitialize()
|
|||||||
{
|
{
|
||||||
Q_ASSERT(!m_currentSession);
|
Q_ASSERT(!m_currentSession);
|
||||||
|
|
||||||
const QString sessionId {parseCookie(m_request.headers.value(u"cookie"_s)).value(m_sessionCookieName)};
|
const QString sessionId {parseCookie(m_request.headers.value(Http::HEADER_COOKIE)).value(m_sessionCookieName)};
|
||||||
|
|
||||||
// TODO: Additional session check
|
// TODO: Additional session check
|
||||||
|
|
||||||
@@ -699,19 +702,36 @@ void WebApplication::sessionInitialize()
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (m_currentSession->shouldRefreshCookie())
|
||||||
|
setSessionCookie();
|
||||||
m_currentSession->updateTimestamp();
|
m_currentSession->updateTimestamp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
qDebug() << Q_FUNC_INFO << "session does not exist!";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_currentSession && !isAuthNeeded())
|
if (!m_currentSession && !isAuthNeeded())
|
||||||
sessionStart();
|
sessionStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WebApplication::setSessionCookie()
|
||||||
|
{
|
||||||
|
// 'Permanent Cookie' still require an expiration date so set it to a date in the distant future
|
||||||
|
const std::chrono::seconds expireDuration = (m_sessionTimeout > 0s) ? m_sessionTimeout : std::chrono::years(1);
|
||||||
|
|
||||||
|
QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toLatin1()};
|
||||||
|
cookie.setExpirationDate(QDateTime::currentDateTime().addDuration(expireDuration));
|
||||||
|
cookie.setHttpOnly(true);
|
||||||
|
cookie.setSecure(m_isSecureCookieEnabled && isOriginTrustworthy()); // [rfc6265] 4.1.2.5. The Secure Attribute
|
||||||
|
cookie.setPath(u"/"_s);
|
||||||
|
if (m_isCSRFProtectionEnabled)
|
||||||
|
cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict);
|
||||||
|
else if (cookie.isSecure())
|
||||||
|
cookie.setSameSitePolicy(QNetworkCookie::SameSite::None);
|
||||||
|
|
||||||
|
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())});
|
||||||
|
m_currentSession->setCookieRefreshTime(expireDuration);
|
||||||
|
}
|
||||||
|
|
||||||
void WebApplication::apiKeySessionInitialize()
|
void WebApplication::apiKeySessionInitialize()
|
||||||
{
|
{
|
||||||
Q_ASSERT(!m_currentSession);
|
Q_ASSERT(!m_currentSession);
|
||||||
@@ -807,17 +827,7 @@ void WebApplication::sessionStartImpl(const QString &sessionId, const bool useCo
|
|||||||
m_currentSession->registerAPIController(u"sync"_s, syncController);
|
m_currentSession->registerAPIController(u"sync"_s, syncController);
|
||||||
|
|
||||||
if (useCookie)
|
if (useCookie)
|
||||||
{
|
setSessionCookie();
|
||||||
QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toLatin1()};
|
|
||||||
cookie.setHttpOnly(true);
|
|
||||||
cookie.setSecure(m_isSecureCookieEnabled && isOriginTrustworthy()); // [rfc6265] 4.1.2.5. The Secure Attribute
|
|
||||||
cookie.setPath(u"/"_s);
|
|
||||||
if (m_isCSRFProtectionEnabled)
|
|
||||||
cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict);
|
|
||||||
else if (cookie.isSecure())
|
|
||||||
cookie.setSameSitePolicy(QNetworkCookie::SameSite::None);
|
|
||||||
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebApplication::sessionEnd()
|
void WebApplication::sessionEnd()
|
||||||
@@ -986,16 +996,29 @@ QString WebSession::id() const
|
|||||||
return m_sid;
|
return m_sid;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WebSession::hasExpired(const qint64 seconds) const
|
bool WebSession::hasExpired(const std::chrono::milliseconds duration) const
|
||||||
{
|
{
|
||||||
if (seconds <= 0)
|
// don't expire for special values
|
||||||
|
if (duration <= 0ms)
|
||||||
return false;
|
return false;
|
||||||
return m_timer.hasExpired(seconds * 1000);
|
return m_timestamp.durationElapsed() > duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSession::updateTimestamp()
|
void WebSession::updateTimestamp()
|
||||||
{
|
{
|
||||||
m_timer.start();
|
m_timestamp.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSession::shouldRefreshCookie() const
|
||||||
|
{
|
||||||
|
return m_cookieRefreshTimer.hasExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSession::setCookieRefreshTime(const std::chrono::seconds timeout)
|
||||||
|
{
|
||||||
|
// Safari browser does not persist cookies for more than 7 days, so we refresh cookies older than 1 day
|
||||||
|
const std::chrono::seconds time = std::min((timeout / 2), std::chrono::duration_cast<std::chrono::seconds>(std::chrono::days(1)));
|
||||||
|
m_cookieRefreshTimer.setRemainingTime(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSession::registerAPIController(const QString &scope, APIController *controller)
|
void WebSession::registerAPIController(const QString &scope, APIController *controller)
|
||||||
|
|||||||
@@ -29,10 +29,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <QDeadlineTimer>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QHostAddress>
|
#include <QHostAddress>
|
||||||
@@ -53,6 +55,8 @@
|
|||||||
#include "base/utils/version.h"
|
#include "base/utils/version.h"
|
||||||
#include "api/isessionmanager.h"
|
#include "api/isessionmanager.h"
|
||||||
|
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
inline const Utils::Version<3, 2> API_VERSION {2, 14, 1};
|
inline const Utils::Version<3, 2> API_VERSION {2, 14, 1};
|
||||||
|
|
||||||
class APIController;
|
class APIController;
|
||||||
@@ -72,15 +76,18 @@ public:
|
|||||||
|
|
||||||
QString id() const override;
|
QString id() const override;
|
||||||
|
|
||||||
bool hasExpired(qint64 seconds) const;
|
bool hasExpired(std::chrono::milliseconds duration) const;
|
||||||
void updateTimestamp();
|
void updateTimestamp();
|
||||||
|
bool shouldRefreshCookie() const;
|
||||||
|
void setCookieRefreshTime(std::chrono::seconds timeout);
|
||||||
|
|
||||||
void registerAPIController(const QString &scope, APIController *controller);
|
void registerAPIController(const QString &scope, APIController *controller);
|
||||||
APIController *getAPIController(const QString &scope) const;
|
APIController *getAPIController(const QString &scope) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const QString m_sid;
|
const QString m_sid;
|
||||||
QElapsedTimer m_timer; // timestamp
|
QElapsedTimer m_timestamp;
|
||||||
|
QDeadlineTimer m_cookieRefreshTimer;
|
||||||
QMap<QString, APIController *> m_apiControllers;
|
QMap<QString, APIController *> m_apiControllers;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,6 +130,7 @@ private:
|
|||||||
// Session management
|
// Session management
|
||||||
QString generateSid() const;
|
QString generateSid() const;
|
||||||
void sessionInitialize();
|
void sessionInitialize();
|
||||||
|
void setSessionCookie();
|
||||||
void apiKeySessionInitialize();
|
void apiKeySessionInitialize();
|
||||||
bool isAuthNeeded();
|
bool isAuthNeeded();
|
||||||
bool isPublicAPI(const QString &scope, const QString &action) const;
|
bool isPublicAPI(const QString &scope, const QString &action) const;
|
||||||
@@ -247,7 +255,7 @@ private:
|
|||||||
bool m_isLocalAuthEnabled = false;
|
bool m_isLocalAuthEnabled = false;
|
||||||
bool m_isAuthSubnetWhitelistEnabled = false;
|
bool m_isAuthSubnetWhitelistEnabled = false;
|
||||||
QList<Utils::Net::Subnet> m_authSubnetWhitelist;
|
QList<Utils::Net::Subnet> m_authSubnetWhitelist;
|
||||||
int m_sessionTimeout = 0;
|
std::chrono::seconds m_sessionTimeout = 0s;
|
||||||
QString m_sessionCookieName;
|
QString m_sessionCookieName;
|
||||||
QString m_apiKey;
|
QString m_apiKey;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user