Skip to content

Commit 0c17d91

Browse files
authored
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.
1 parent 60feb3c commit 0c17d91

File tree

3 files changed

+58
-26
lines changed

3 files changed

+58
-26
lines changed

src/base/http/types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ namespace Http
5151
inline const QString HEADER_CONTENT_LENGTH = u"content-length"_s;
5252
inline const QString HEADER_CONTENT_SECURITY_POLICY = u"content-security-policy"_s;
5353
inline const QString HEADER_CONTENT_TYPE = u"content-type"_s;
54+
inline const QString HEADER_COOKIE = u"cookie"_s;
5455
inline const QString HEADER_CROSS_ORIGIN_OPENER_POLICY = u"cross-origin-opener-policy"_s;
5556
inline const QString HEADER_DATE = u"date"_s;
5657
inline const QString HEADER_HOST = u"host"_s;

src/webui/webapplication.cpp

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
#include "webapplication.h"
3131

3232
#include <algorithm>
33-
#include <chrono>
3433

3534
#include <QDateTime>
3635
#include <QDebug>
@@ -88,7 +87,7 @@ namespace
8887
QStringMap ret;
8988
const QList<QStringView> cookies = cookieStr.split(u';', Qt::SkipEmptyParts);
9089

91-
for (const auto &cookie : cookies)
90+
for (const QStringView cookie : cookies)
9291
{
9392
const qsizetype idx = cookie.indexOf(u'=');
9493
if (idx < 0)
@@ -462,9 +461,13 @@ void WebApplication::configure()
462461
m_isLocalAuthEnabled = pref->isWebUILocalAuthEnabled();
463462
m_isAuthSubnetWhitelistEnabled = pref->isWebUIAuthSubnetWhitelistEnabled();
464463
m_authSubnetWhitelist = pref->getWebUIAuthSubnetWhitelist();
465-
m_sessionTimeout = pref->getWebUISessionTimeout();
464+
m_sessionTimeout = std::chrono::seconds(pref->getWebUISessionTimeout());
466465
m_sessionCookieName = SESSION_COOKIE_NAME_PREFIX + QString::number(pref->getWebUIPort());
467466

467+
// all sessions need to update the cookie expiration date
468+
for (WebSession *session : asConst(m_sessions))
469+
session->setCookieRefreshTime(0s);
470+
468471
m_domainList = pref->getServerDomains().split(u';', Qt::SkipEmptyParts);
469472
for (QString &entry : m_domainList)
470473
entry = entry.trimmed();
@@ -682,7 +685,7 @@ void WebApplication::sessionInitialize()
682685
{
683686
Q_ASSERT(!m_currentSession);
684687

685-
const QString sessionId {parseCookie(m_request.headers.value(u"cookie"_s)).value(m_sessionCookieName)};
688+
const QString sessionId {parseCookie(m_request.headers.value(Http::HEADER_COOKIE)).value(m_sessionCookieName)};
686689

687690
// TODO: Additional session check
688691

@@ -699,19 +702,36 @@ void WebApplication::sessionInitialize()
699702
}
700703
else
701704
{
705+
if (m_currentSession->shouldRefreshCookie())
706+
setSessionCookie();
702707
m_currentSession->updateTimestamp();
703708
}
704709
}
705-
else
706-
{
707-
qDebug() << Q_FUNC_INFO << "session does not exist!";
708-
}
709710
}
710711

711712
if (!m_currentSession && !isAuthNeeded())
712713
sessionStart();
713714
}
714715

716+
void WebApplication::setSessionCookie()
717+
{
718+
// 'Permanent Cookie' still require an expiration date so set it to a date in the distant future
719+
const std::chrono::seconds expireDuration = (m_sessionTimeout > 0s) ? m_sessionTimeout : std::chrono::years(1);
720+
721+
QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toLatin1()};
722+
cookie.setExpirationDate(QDateTime::currentDateTime().addDuration(expireDuration));
723+
cookie.setHttpOnly(true);
724+
cookie.setSecure(m_isSecureCookieEnabled && isOriginTrustworthy()); // [rfc6265] 4.1.2.5. The Secure Attribute
725+
cookie.setPath(u"/"_s);
726+
if (m_isCSRFProtectionEnabled)
727+
cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict);
728+
else if (cookie.isSecure())
729+
cookie.setSameSitePolicy(QNetworkCookie::SameSite::None);
730+
731+
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())});
732+
m_currentSession->setCookieRefreshTime(expireDuration);
733+
}
734+
715735
void WebApplication::apiKeySessionInitialize()
716736
{
717737
Q_ASSERT(!m_currentSession);
@@ -807,17 +827,7 @@ void WebApplication::sessionStartImpl(const QString &sessionId, const bool useCo
807827
m_currentSession->registerAPIController(u"sync"_s, syncController);
808828

809829
if (useCookie)
810-
{
811-
QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toLatin1()};
812-
cookie.setHttpOnly(true);
813-
cookie.setSecure(m_isSecureCookieEnabled && isOriginTrustworthy()); // [rfc6265] 4.1.2.5. The Secure Attribute
814-
cookie.setPath(u"/"_s);
815-
if (m_isCSRFProtectionEnabled)
816-
cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict);
817-
else if (cookie.isSecure())
818-
cookie.setSameSitePolicy(QNetworkCookie::SameSite::None);
819-
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())});
820-
}
830+
setSessionCookie();
821831
}
822832

823833
void WebApplication::sessionEnd()
@@ -986,16 +996,29 @@ QString WebSession::id() const
986996
return m_sid;
987997
}
988998

989-
bool WebSession::hasExpired(const qint64 seconds) const
999+
bool WebSession::hasExpired(const std::chrono::milliseconds duration) const
9901000
{
991-
if (seconds <= 0)
1001+
// don't expire for special values
1002+
if (duration <= 0ms)
9921003
return false;
993-
return m_timer.hasExpired(seconds * 1000);
1004+
return m_timestamp.durationElapsed() > duration;
9941005
}
9951006

9961007
void WebSession::updateTimestamp()
9971008
{
998-
m_timer.start();
1009+
m_timestamp.start();
1010+
}
1011+
1012+
bool WebSession::shouldRefreshCookie() const
1013+
{
1014+
return m_cookieRefreshTimer.hasExpired();
1015+
}
1016+
1017+
void WebSession::setCookieRefreshTime(const std::chrono::seconds timeout)
1018+
{
1019+
// Safari browser does not persist cookies for more than 7 days, so we refresh cookies older than 1 day
1020+
const std::chrono::seconds time = std::min((timeout / 2), std::chrono::duration_cast<std::chrono::seconds>(std::chrono::days(1)));
1021+
m_cookieRefreshTimer.setRemainingTime(time);
9991022
}
10001023

10011024
void WebSession::registerAPIController(const QString &scope, APIController *controller)

src/webui/webapplication.h

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929

3030
#pragma once
3131

32+
#include <chrono>
3233
#include <type_traits>
3334
#include <utility>
3435

3536
#include <QDateTime>
37+
#include <QDeadlineTimer>
3638
#include <QElapsedTimer>
3739
#include <QHash>
3840
#include <QHostAddress>
@@ -53,6 +55,8 @@
5355
#include "base/utils/version.h"
5456
#include "api/isessionmanager.h"
5557

58+
using namespace std::chrono_literals;
59+
5660
inline const Utils::Version<3, 2> API_VERSION {2, 14, 1};
5761

5862
class APIController;
@@ -72,15 +76,18 @@ class WebSession final : public ApplicationComponent<QObject>, public ISession
7276

7377
QString id() const override;
7478

75-
bool hasExpired(qint64 seconds) const;
79+
bool hasExpired(std::chrono::milliseconds duration) const;
7680
void updateTimestamp();
81+
bool shouldRefreshCookie() const;
82+
void setCookieRefreshTime(std::chrono::seconds timeout);
7783

7884
void registerAPIController(const QString &scope, APIController *controller);
7985
APIController *getAPIController(const QString &scope) const;
8086

8187
private:
8288
const QString m_sid;
83-
QElapsedTimer m_timer; // timestamp
89+
QElapsedTimer m_timestamp;
90+
QDeadlineTimer m_cookieRefreshTimer;
8491
QMap<QString, APIController *> m_apiControllers;
8592
};
8693

@@ -123,6 +130,7 @@ class WebApplication final : public ApplicationComponent<QObject>
123130
// Session management
124131
QString generateSid() const;
125132
void sessionInitialize();
133+
void setSessionCookie();
126134
void apiKeySessionInitialize();
127135
bool isAuthNeeded();
128136
bool isPublicAPI(const QString &scope, const QString &action) const;
@@ -247,7 +255,7 @@ class WebApplication final : public ApplicationComponent<QObject>
247255
bool m_isLocalAuthEnabled = false;
248256
bool m_isAuthSubnetWhitelistEnabled = false;
249257
QList<Utils::Net::Subnet> m_authSubnetWhitelist;
250-
int m_sessionTimeout = 0;
258+
std::chrono::seconds m_sessionTimeout = 0s;
251259
QString m_sessionCookieName;
252260
QString m_apiKey;
253261

0 commit comments

Comments
 (0)