diff --git a/src/base/http/types.h b/src/base/http/types.h index 2700945eb..ddaa1ccd1 100644 --- a/src/base/http/types.h +++ b/src/base/http/types.h @@ -51,6 +51,7 @@ namespace Http 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_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_DATE = u"date"_s; inline const QString HEADER_HOST = u"host"_s; diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index 1233b03d9..c6de1586e 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -30,7 +30,6 @@ #include "webapplication.h" #include -#include #include #include @@ -88,7 +87,7 @@ namespace QStringMap ret; const QList cookies = cookieStr.split(u';', Qt::SkipEmptyParts); - for (const auto &cookie : cookies) + for (const QStringView cookie : cookies) { const qsizetype idx = cookie.indexOf(u'='); if (idx < 0) @@ -462,9 +461,13 @@ void WebApplication::configure() m_isLocalAuthEnabled = pref->isWebUILocalAuthEnabled(); m_isAuthSubnetWhitelistEnabled = pref->isWebUIAuthSubnetWhitelistEnabled(); 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()); + // 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); for (QString &entry : m_domainList) entry = entry.trimmed(); @@ -682,7 +685,7 @@ void WebApplication::sessionInitialize() { 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 @@ -699,19 +702,36 @@ void WebApplication::sessionInitialize() } else { + if (m_currentSession->shouldRefreshCookie()) + setSessionCookie(); m_currentSession->updateTimestamp(); } } - else - { - qDebug() << Q_FUNC_INFO << "session does not exist!"; - } } if (!m_currentSession && !isAuthNeeded()) 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() { Q_ASSERT(!m_currentSession); @@ -807,17 +827,7 @@ void WebApplication::sessionStartImpl(const QString &sessionId, const bool useCo m_currentSession->registerAPIController(u"sync"_s, syncController); if (useCookie) - { - 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())}); - } + setSessionCookie(); } void WebApplication::sessionEnd() @@ -986,16 +996,29 @@ QString WebSession::id() const 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 m_timer.hasExpired(seconds * 1000); + return m_timestamp.durationElapsed() > duration; } 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::days(1))); + m_cookieRefreshTimer.setRemainingTime(time); } void WebSession::registerAPIController(const QString &scope, APIController *controller) diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 032491a18..ebac6207b 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -29,10 +29,12 @@ #pragma once +#include #include #include #include +#include #include #include #include @@ -53,6 +55,8 @@ #include "base/utils/version.h" #include "api/isessionmanager.h" +using namespace std::chrono_literals; + inline const Utils::Version<3, 2> API_VERSION {2, 14, 1}; class APIController; @@ -72,15 +76,18 @@ public: QString id() const override; - bool hasExpired(qint64 seconds) const; + bool hasExpired(std::chrono::milliseconds duration) const; void updateTimestamp(); + bool shouldRefreshCookie() const; + void setCookieRefreshTime(std::chrono::seconds timeout); void registerAPIController(const QString &scope, APIController *controller); APIController *getAPIController(const QString &scope) const; private: const QString m_sid; - QElapsedTimer m_timer; // timestamp + QElapsedTimer m_timestamp; + QDeadlineTimer m_cookieRefreshTimer; QMap m_apiControllers; }; @@ -123,6 +130,7 @@ private: // Session management QString generateSid() const; void sessionInitialize(); + void setSessionCookie(); void apiKeySessionInitialize(); bool isAuthNeeded(); bool isPublicAPI(const QString &scope, const QString &action) const; @@ -247,7 +255,7 @@ private: bool m_isLocalAuthEnabled = false; bool m_isAuthSubnetWhitelistEnabled = false; QList m_authSubnetWhitelist; - int m_sessionTimeout = 0; + std::chrono::seconds m_sessionTimeout = 0s; QString m_sessionCookieName; QString m_apiKey;