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:
Chocobo1
2025-10-25 03:37:46 +08:00
committed by GitHub
parent 60feb3cce2
commit 0c17d91d63
3 changed files with 58 additions and 26 deletions

View File

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

View File

@@ -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)

View File

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