6#include "sessionstorefile.h"
9#include <Cutelyst/Application>
10#include <Cutelyst/Context>
11#include <Cutelyst/Engine>
12#include <Cutelyst/Response>
14#include <QCoreApplication>
15#include <QHostAddress>
16#include <QLoggingCategory>
20using namespace Qt::Literals::StringLiterals;
22Q_LOGGING_CATEGORY(C_SESSION,
"cutelyst.plugin.session", QtWarningMsg)
24#define SESSION_VALUES u"_c_session_values"_s
25#define SESSION_EXPIRES u"_c_session_expires"_s
26#define SESSION_TRIED_LOADING_EXPIRES u"_c_session_tried_loading_expires"_s
27#define SESSION_EXTENDED_EXPIRES u"_c_session_extended_expires"_s
28#define SESSION_UPDATED u"_c_session_updated"_s
29#define SESSION_ID u"_c_session_id"_s
30#define SESSION_TRIED_LOADING_ID u"_c_session_tried_loading_id"_s
31#define SESSION_DELETED_ID u"_c_session_deleted_id"_s
32#define SESSION_DELETE_REASON u"_c_session_delete_reason"_s
35thread_local Session *m_instance =
nullptr;
40 , d_ptr(new SessionPrivate(this))
46 , d_ptr(new SessionPrivate(this))
48 d_ptr->defaultConfig = defaultConfig;
59 d->sessionName = QCoreApplication::applicationName().toLatin1() +
"_session";
61 d->loadedConfig = app->
engine()->
config(u
"Cutelyst_Session_Plugin"_s);
62 d->sessionExpires = std::chrono::duration_cast<std::chrono::seconds>(
65 d->expiryThreshold = d->config(u
"expiry_threshold"_s, 0).toLongLong();
66 d->verifyAddress = d->config(u
"verify_address"_s,
false).toBool();
67 d->verifyUserAgent = d->config(u
"verify_user_agent"_s,
false).toBool();
68 d->cookieHttpOnly = d->config(u
"cookie_http_only"_s,
true).toBool();
69 d->cookieSecure = d->config(u
"cookie_secure"_s,
false).toBool();
71 const QString _sameSite = d->config(u
"cookie_same_site"_s, u
"strict"_s).toString();
72 if (_sameSite.compare(u
"default", Qt::CaseInsensitive) == 0) {
73 d->cookieSameSite = QNetworkCookie::SameSite::Default;
74 }
else if (_sameSite.compare(u
"none", Qt::CaseInsensitive) == 0) {
75 d->cookieSameSite = QNetworkCookie::SameSite::None;
76 }
else if (_sameSite.compare(u
"lax", Qt::CaseInsensitive) == 0) {
77 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
79 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
86 d->store = std::make_unique<SessionStoreFile>(
this);
95 Q_ASSERT_X(d->store,
"Cutelyst::Session::setStorage",
"Session Storage is alread defined");
96 store->setParent(
this);
97 d->store = std::move(store);
103 return d->store.get();
109 const QVariant sid = c->
stash(SESSION_ID);
111 if (Q_UNLIKELY(!m_instance)) {
112 qCCritical(C_SESSION) <<
"Session plugin not registered";
116 ret = SessionPrivate::loadSessionId(c, m_instance->d_ptr->sessionName);
118 ret = sid.toByteArray();
131 if (Q_UNLIKELY(!m_instance)) {
132 qCCritical(C_SESSION) <<
"Session plugin not registered";
136 expires = SessionPrivate::loadSessionExpires(m_instance, c,
id(c));
138 return SessionPrivate::extendSessionExpires(m_instance, c,
expires.toLongLong());
147 const qint64 timeExp = QDateTime::currentSecsSinceEpoch() +
expires;
149 if (Q_UNLIKELY(!m_instance)) {
150 qCCritical(C_SESSION) <<
"Session plugin not registered";
154 m_instance->d_ptr->store->storeSessionData(c, sid, u
"expires"_s, timeExp);
159 if (Q_UNLIKELY(!m_instance)) {
160 qCCritical(C_SESSION) <<
"Session plugin not registered";
163 SessionPrivate::deleteSession(m_instance, c, reason);
168 return c->
stash(SESSION_DELETE_REASON).toString();
173 QVariant ret = defaultValue;
174 QVariant session = c->
stash(SESSION_VALUES);
175 if (session.isNull()) {
176 session = SessionPrivate::loadSession(c);
179 if (!session.isNull()) {
180 ret = session.toHash().value(key, defaultValue);
188 QVariant session = c->
stash(SESSION_VALUES);
189 if (session.isNull()) {
190 session = SessionPrivate::loadSession(c);
191 if (session.isNull()) {
192 if (Q_UNLIKELY(!m_instance)) {
193 qCCritical(C_SESSION) <<
"Session plugin not registered";
197 SessionPrivate::createSessionIdIfNeeded(
198 m_instance, c, m_instance->d_ptr->sessionExpires);
199 session = SessionPrivate::initializeSessionData(m_instance, c);
203 QVariantHash data = session.toHash();
204 data.insert(key,
value);
212 QVariant session = c->
stash(SESSION_VALUES);
213 if (session.isNull()) {
214 session = SessionPrivate::loadSession(c);
215 if (session.isNull()) {
216 if (Q_UNLIKELY(!m_instance)) {
217 qCCritical(C_SESSION) <<
"Session plugin not registered";
221 SessionPrivate::createSessionIdIfNeeded(
222 m_instance, c, m_instance->d_ptr->sessionExpires);
223 session = SessionPrivate::initializeSessionData(m_instance, c);
227 QVariantHash data = session.toHash();
236 QVariant session = c->
stash(SESSION_VALUES);
237 if (session.isNull()) {
238 session = SessionPrivate::loadSession(c);
239 if (session.isNull()) {
240 if (Q_UNLIKELY(!m_instance)) {
241 qCCritical(C_SESSION) <<
"Session plugin not registered";
245 SessionPrivate::createSessionIdIfNeeded(
246 m_instance, c, m_instance->d_ptr->sessionExpires);
247 session = SessionPrivate::initializeSessionData(m_instance, c);
251 QVariantHash data = session.toHash();
252 for (
const QString &key : keys) {
262 return !SessionPrivate::loadSession(c).isNull();
265QByteArray SessionPrivate::generateSessionId()
267 return QUuid::createUuid().toRfc4122().toHex();
270QByteArray SessionPrivate::loadSessionId(
Context *c,
const QByteArray &sessionName)
273 if (!c->
stash(SESSION_TRIED_LOADING_ID).isNull()) {
276 c->
setStash(SESSION_TRIED_LOADING_ID,
true);
278 const QByteArray sid = getSessionId(c, sessionName);
279 if (!sid.isEmpty()) {
280 if (!validateSessionId(sid)) {
281 qCCritical(C_SESSION) <<
"Tried to set invalid session ID" << sid;
291QByteArray SessionPrivate::getSessionId(
const Context *c,
const QByteArray &sessionName)
294 bool deleted = !c->
stash(SESSION_DELETED_ID).isNull();
297 const QVariant
property = c->
stash(SESSION_ID);
298 if (!property.isNull()) {
299 ret =
property.toByteArray();
304 if (!cookie.isEmpty()) {
305 qCDebug(C_SESSION) <<
"Found sessionid" << cookie <<
"in cookie";
313QByteArray SessionPrivate::createSessionIdIfNeeded(
Session *session,
Context *c, qint64 expires)
316 const QVariant sid = c->
stash(SESSION_ID);
318 ret = sid.toByteArray();
320 ret = createSessionId(session, c, expires);
325QByteArray SessionPrivate::createSessionId(
Session *session,
Context *c, qint64 expires)
328 const auto sid = generateSessionId();
330 qCDebug(C_SESSION) <<
"Created session" << sid;
333 resetSessionExpires(session, c, sid);
334 setSessionId(session, c, sid);
339void SessionPrivate::_q_saveSession(
Context *c)
342 saveSessionExpires(c);
350 if (Q_UNLIKELY(!m_instance)) {
351 qCCritical(C_SESSION) <<
"Session plugin not registered";
354 saveSessionExpires(c);
356 if (!c->
stash(SESSION_UPDATED).toBool()) {
359 QVariantHash sessionData = c->
stash(SESSION_VALUES).toHash();
360 sessionData.insert(u
"__updated"_s, QDateTime::currentSecsSinceEpoch());
362 const auto sid = c->
stash(SESSION_ID).toByteArray();
363 m_instance->d_ptr->store->storeSessionData(c, sid, u
"session"_s, sessionData);
366void SessionPrivate::deleteSession(
Session *session,
Context *c,
const QString &reason)
368 qCDebug(C_SESSION) <<
"Deleting session" << reason;
370 const QVariant sidVar = c->
stash(SESSION_ID).toString();
371 if (!sidVar.isNull()) {
372 const auto sid = sidVar.toByteArray();
373 session->d_ptr->store->deleteSessionData(c, sid, u
"session"_s);
374 session->d_ptr->store->deleteSessionData(c, sid, u
"expires"_s);
375 session->d_ptr->store->deleteSessionData(c, sid, u
"flash"_s);
377 deleteSessionId(session, c, sid);
381 c->
setStash(SESSION_VALUES, QVariant());
382 c->
setStash(SESSION_ID, QVariant());
383 c->
setStash(SESSION_EXPIRES, QVariant());
385 c->
setStash(SESSION_DELETE_REASON, reason);
388void SessionPrivate::deleteSessionId(
Session *session,
Context *c,
const QByteArray &sid)
390 c->
setStash(SESSION_DELETED_ID,
true);
392 updateSessionCookie(c, makeSessionCookie(session, c, sid, QDateTime::currentDateTimeUtc()));
395QVariant SessionPrivate::loadSession(
Context *c)
398 const QVariant
property = c->
stash(SESSION_VALUES);
399 if (!property.isNull()) {
400 ret =
property.toHash();
404 if (Q_UNLIKELY(!m_instance)) {
405 qCCritical(C_SESSION) <<
"Session plugin not registered";
410 if (!loadSessionExpires(m_instance, c, sid).isNull()) {
411 if (SessionPrivate::validateSessionId(sid)) {
413 const QVariantHash sessionData =
414 m_instance->d_ptr->store->getSessionData(c, sid, u
"session"_s).toHash();
415 c->
setStash(SESSION_VALUES, sessionData);
417 if (m_instance->d_ptr->verifyAddress) {
418 auto it = sessionData.constFind(u
"__address"_s);
419 if (it != sessionData.constEnd() &&
422 <<
"Deleting session" << sid <<
"due to address mismatch:" << *it
424 deleteSession(m_instance, c, u
"address mismatch"_s);
429 if (m_instance->d_ptr->verifyUserAgent) {
430 auto it = sessionData.constFind(u
"__user_agent"_s);
431 if (it != sessionData.constEnd() &&
432 it->toByteArray() != c->
request()->userAgent()) {
434 <<
"Deleting session" << sid <<
"due to user agent mismatch:" << *it
435 <<
"!=" << c->
request()->userAgent();
436 deleteSession(m_instance, c, u
"user agent mismatch"_s);
441 qCDebug(C_SESSION) <<
"Restored session" << sid <<
"keys" << sessionData.size();
450bool SessionPrivate::validateSessionId(QByteArrayView
id)
452 return !
id.empty() && std::ranges::all_of(
id, [](
char c) {
453 return (c >=
'a' && c <=
'f') || (c >=
'0' && c <=
'9');
457qint64 SessionPrivate::extendSessionExpires(
Session *session,
Context *c, qint64 expires)
459 const qint64 threshold = session->d_ptr->expiryThreshold;
462 if (!sid.isEmpty()) {
463 const qint64 current = getStoredSessionExpires(session, c, sid);
464 const qint64 cutoff = current - threshold;
465 const qint64 time = QDateTime::currentSecsSinceEpoch();
467 if (!threshold || cutoff <= time || c->stash(SESSION_UPDATED).toBool()) {
468 qint64 updated = calculateInitialSessionExpires(session, c, sid);
469 c->
setStash(SESSION_EXTENDED_EXPIRES, updated);
470 extendSessionId(session, c, sid, updated);
481qint64 SessionPrivate::getStoredSessionExpires(
Session *session,
483 const QByteArray &sessionid)
485 const QVariant expires = session->d_ptr->store->getSessionData(c, sessionid, u
"expires"_s, 0);
486 return expires.toLongLong();
489QVariant SessionPrivate::initializeSessionData(
const Session *session,
const Context *c)
492 const qint64 now = QDateTime::currentSecsSinceEpoch();
493 ret.insert(u
"__created"_s, now);
494 ret.insert(u
"__updated"_s, now);
496 if (session->d_ptr->verifyAddress) {
500 if (session->d_ptr->verifyUserAgent) {
501 ret.insert(u
"__user_agent"_s, c->
request()->userAgent());
507void SessionPrivate::saveSessionExpires(
Context *c)
509 const QVariant expires = c->
stash(SESSION_EXPIRES);
510 if (!expires.isNull()) {
512 if (!sid.isEmpty()) {
513 if (Q_UNLIKELY(!m_instance)) {
514 qCCritical(C_SESSION) <<
"Session plugin not registered";
518 const qint64 current = getStoredSessionExpires(m_instance, c, sid);
520 if (extended > current) {
521 m_instance->d_ptr->store->storeSessionData(c, sid, u
"expires"_s, extended);
528 SessionPrivate::loadSessionExpires(
Session *session,
Context *c,
const QByteArray &sessionId)
531 if (c->
stash(SESSION_TRIED_LOADING_EXPIRES).toBool()) {
532 ret = c->
stash(SESSION_EXPIRES);
535 c->
setStash(SESSION_TRIED_LOADING_EXPIRES,
true);
537 if (!sessionId.isEmpty()) {
538 const qint64 expires = getStoredSessionExpires(session, c, sessionId);
540 if (expires >= QDateTime::currentSecsSinceEpoch()) {
541 c->
setStash(SESSION_EXPIRES, expires);
544 deleteSession(session, c, u
"session expired"_s);
551qint64 SessionPrivate::initialSessionExpires(
Session *session,
Context *c)
554 const qint64 expires = session->d_ptr->sessionExpires;
555 return QDateTime::currentSecsSinceEpoch() + expires;
558qint64 SessionPrivate::calculateInitialSessionExpires(
Session *session,
560 const QByteArray &sessionId)
562 const qint64 stored = getStoredSessionExpires(session, c, sessionId);
563 const qint64 initial = initialSessionExpires(session, c);
564 return qMax(initial, stored);
568 SessionPrivate::resetSessionExpires(
Session *session,
Context *c,
const QByteArray &sessionId)
570 const qint64 exp = calculateInitialSessionExpires(session, c, sessionId);
576 c->
setStash(SESSION_TRIED_LOADING_EXPIRES,
true);
577 c->
setStash(SESSION_EXTENDED_EXPIRES, exp);
582void SessionPrivate::updateSessionCookie(
const Context *c,
const QNetworkCookie &updated)
587QNetworkCookie SessionPrivate::makeSessionCookie(
Session *session,
589 const QByteArray &sid,
590 const QDateTime &expires)
593 QNetworkCookie cookie(session->d_ptr->sessionName, sid);
594 cookie.setPath(u
"/"_s);
595 cookie.setExpirationDate(expires);
596 cookie.setHttpOnly(session->d_ptr->cookieHttpOnly);
597 cookie.setSecure(session->d_ptr->cookieSecure);
598 cookie.setSameSitePolicy(session->d_ptr->cookieSameSite);
603void SessionPrivate::extendSessionId(
Session *session,
605 const QByteArray &sid,
608 updateSessionCookie(c,
609 makeSessionCookie(session, c, sid, QDateTime::fromSecsSinceEpoch(expires)));
612void SessionPrivate::setSessionId(
Session *session,
Context *c,
const QByteArray &sid)
617 session, c, sid, QDateTime::fromSecsSinceEpoch(initialSessionExpires(session, c))));
620QVariant SessionPrivate::config(
const QString &key,
const QVariant &defaultValue)
const
622 return loadedConfig.value(key, defaultConfig.value(key, defaultValue));
630#include "moc_session.cpp"
The Cutelyst application.
Engine * engine() const noexcept
void afterDispatch(Cutelyst::Context *c)
void postForked(Cutelyst::Application *app)
void stash(const QVariantHash &unite)
void setStash(const QString &key, const QVariant &value)
Response * response() const noexcept
QVariantMap config(const QString &entity) const
Plugin(Application *parent)
QByteArray cookie(QAnyStringView name) const
QHostAddress address() const noexcept
void setCookie(const QNetworkCookie &cookie)
Abstract class to create a session store.
SessionStore(QObject *parent=nullptr)
Plugin providing methods for session management.
static qint64 expires(Context *c)
static QString deleteReason(Context *c)
virtual bool setup(Application *app) final
Session(Application *parent)
static bool isValid(Context *c)
static void deleteSession(Context *c, const QString &reason={})
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
static void setValue(Context *c, const QString &key, const QVariant &value)
void setStorage(std::unique_ptr< SessionStore > store)
static QByteArray id(Context *c)
SessionStore * storage() const
static void deleteValue(Context *c, const QString &key)
static void deleteValues(Context *c, const QStringList &keys)
static void changeExpires(Context *c, qint64 expires)
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
The Cutelyst namespace holds all public Cutelyst API.