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 QStringLiteral("_c_session_values")
25#define SESSION_EXPIRES QStringLiteral("_c_session_expires")
26#define SESSION_TRIED_LOADING_EXPIRES QStringLiteral("_c_session_tried_loading_expires")
27#define SESSION_EXTENDED_EXPIRES QStringLiteral("_c_session_extended_expires")
28#define SESSION_UPDATED QStringLiteral("_c_session_updated")
29#define SESSION_ID QStringLiteral("_c_session_id")
30#define SESSION_TRIED_LOADING_ID QStringLiteral("_c_session_tried_loading_id")
31#define SESSION_DELETED_ID QStringLiteral("_c_session_deleted_id")
32#define SESSION_DELETE_REASON QStringLiteral("_c_session_delete_reason")
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(
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(QStringLiteral(
"__updated"), QDateTime::currentSecsSinceEpoch());
362 const auto sid = c->
stash(SESSION_ID).toByteArray();
363 m_instance->d_ptr->store->storeSessionData(c, sid, QStringLiteral(
"session"), 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, QStringLiteral(
"session"));
374 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral(
"expires"));
375 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral(
"flash"));
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, QStringLiteral(
"session"))
416 c->
setStash(SESSION_VALUES, sessionData);
418 if (m_instance->d_ptr->verifyAddress) {
419 auto it = sessionData.constFind(u
"__address"_s);
420 if (it != sessionData.constEnd() &&
423 <<
"Deleting session" << sid <<
"due to address mismatch:" << *it
425 deleteSession(m_instance, c, QStringLiteral(
"address mismatch"));
430 if (m_instance->d_ptr->verifyUserAgent) {
431 auto it = sessionData.constFind(u
"__user_agent"_s);
432 if (it != sessionData.constEnd() &&
433 it->toByteArray() != c->
request()->userAgent()) {
435 <<
"Deleting session" << sid <<
"due to user agent mismatch:" << *it
436 <<
"!=" << c->
request()->userAgent();
437 deleteSession(m_instance, c, QStringLiteral(
"user agent mismatch"));
442 qCDebug(C_SESSION) <<
"Restored session" << sid <<
"keys" << sessionData.size();
451bool SessionPrivate::validateSessionId(QByteArrayView
id)
454 if ((c >=
'a' && c <=
'f') || (c >=
'0' && c <=
'9')) {
463qint64 SessionPrivate::extendSessionExpires(
Session *session,
Context *c, qint64 expires)
465 const qint64 threshold = session->d_ptr->expiryThreshold;
468 if (!sid.isEmpty()) {
469 const qint64 current = getStoredSessionExpires(session, c, sid);
470 const qint64 cutoff = current - threshold;
471 const qint64 time = QDateTime::currentSecsSinceEpoch();
473 if (!threshold || cutoff <= time || c->stash(SESSION_UPDATED).toBool()) {
474 qint64 updated = calculateInitialSessionExpires(session, c, sid);
475 c->
setStash(SESSION_EXTENDED_EXPIRES, updated);
476 extendSessionId(session, c, sid, updated);
487qint64 SessionPrivate::getStoredSessionExpires(
Session *session,
489 const QByteArray &sessionid)
491 const QVariant expires =
492 session->d_ptr->store->getSessionData(c, sessionid, QStringLiteral(
"expires"), 0);
493 return expires.toLongLong();
496QVariant SessionPrivate::initializeSessionData(
Session *session,
Context *c)
499 const qint64 now = QDateTime::currentSecsSinceEpoch();
500 ret.insert(QStringLiteral(
"__created"), now);
501 ret.insert(QStringLiteral(
"__updated"), now);
503 if (session->d_ptr->verifyAddress) {
504 ret.insert(QStringLiteral(
"__address"), c->
request()->
address().toString());
507 if (session->d_ptr->verifyUserAgent) {
508 ret.insert(QStringLiteral(
"__user_agent"), c->
request()->userAgent());
514void SessionPrivate::saveSessionExpires(
Context *c)
516 const QVariant expires = c->
stash(SESSION_EXPIRES);
517 if (!expires.isNull()) {
519 if (!sid.isEmpty()) {
520 if (Q_UNLIKELY(!m_instance)) {
521 qCCritical(C_SESSION) <<
"Session plugin not registered";
525 const qint64 current = getStoredSessionExpires(m_instance, c, sid);
527 if (extended > current) {
528 m_instance->d_ptr->store->storeSessionData(
529 c, sid, QStringLiteral(
"expires"), extended);
536 SessionPrivate::loadSessionExpires(
Session *session,
Context *c,
const QByteArray &sessionId)
539 if (c->
stash(SESSION_TRIED_LOADING_EXPIRES).toBool()) {
540 ret = c->
stash(SESSION_EXPIRES);
543 c->
setStash(SESSION_TRIED_LOADING_EXPIRES,
true);
545 if (!sessionId.isEmpty()) {
546 const qint64 expires = getStoredSessionExpires(session, c, sessionId);
548 if (expires >= QDateTime::currentSecsSinceEpoch()) {
549 c->
setStash(SESSION_EXPIRES, expires);
552 deleteSession(session, c, QStringLiteral(
"session expired"));
559qint64 SessionPrivate::initialSessionExpires(
Session *session,
Context *c)
562 const qint64 expires = session->d_ptr->sessionExpires;
563 return QDateTime::currentSecsSinceEpoch() + expires;
566qint64 SessionPrivate::calculateInitialSessionExpires(
Session *session,
568 const QByteArray &sessionId)
570 const qint64 stored = getStoredSessionExpires(session, c, sessionId);
571 const qint64 initial = initialSessionExpires(session, c);
572 return qMax(initial, stored);
576 SessionPrivate::resetSessionExpires(
Session *session,
Context *c,
const QByteArray &sessionId)
578 const qint64 exp = calculateInitialSessionExpires(session, c, sessionId);
584 c->
setStash(SESSION_TRIED_LOADING_EXPIRES,
true);
585 c->
setStash(SESSION_EXTENDED_EXPIRES, exp);
590void SessionPrivate::updateSessionCookie(
Context *c,
const QNetworkCookie &updated)
595QNetworkCookie SessionPrivate::makeSessionCookie(
Session *session,
597 const QByteArray &sid,
598 const QDateTime &expires)
601 QNetworkCookie cookie(session->d_ptr->sessionName, sid);
602 cookie.setPath(u
"/"_s);
603 cookie.setExpirationDate(expires);
604 cookie.setHttpOnly(session->d_ptr->cookieHttpOnly);
605 cookie.setSecure(session->d_ptr->cookieSecure);
606 cookie.setSameSitePolicy(session->d_ptr->cookieSameSite);
611void SessionPrivate::extendSessionId(
Session *session,
613 const QByteArray &sid,
616 updateSessionCookie(c,
617 makeSessionCookie(session, c, sid, QDateTime::fromSecsSinceEpoch(expires)));
620void SessionPrivate::setSessionId(
Session *session,
Context *c,
const QByteArray &sid)
625 session, c, sid, QDateTime::fromSecsSinceEpoch(initialSessionExpires(session, c))));
628QVariant SessionPrivate::config(
const QString &key,
const QVariant &defaultValue)
const
630 return loadedConfig.value(key, defaultConfig.value(key, defaultValue));
638#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
Base class for Cutelyst Plugins.
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.