6#include "sessionstorefile.h"
8#include <Cutelyst/Application>
9#include <Cutelyst/Context>
10#include <Cutelyst/Engine>
11#include <Cutelyst/Response>
13#include <QCoreApplication>
14#include <QHostAddress>
15#include <QLoggingCategory>
20Q_LOGGING_CATEGORY(C_SESSION,
"cutelyst.plugin.session", QtWarningMsg)
22#define SESSION_VALUES QStringLiteral("_c_session_values")
23#define SESSION_EXPIRES QStringLiteral("_c_session_expires")
24#define SESSION_TRIED_LOADING_EXPIRES QStringLiteral("_c_session_tried_loading_expires")
25#define SESSION_EXTENDED_EXPIRES QStringLiteral("_c_session_extended_expires")
26#define SESSION_UPDATED QStringLiteral("_c_session_updated")
27#define SESSION_ID QStringLiteral("_c_session_id")
28#define SESSION_TRIED_LOADING_ID QStringLiteral("_c_session_tried_loading_id")
29#define SESSION_DELETED_ID QStringLiteral("_c_session_deleted_id")
30#define SESSION_DELETE_REASON QStringLiteral("_c_session_delete_reason")
32static thread_local Session *m_instance =
nullptr;
36 , d_ptr(new SessionPrivate(this))
40Cutelyst::Session::~Session()
48 d->sessionName = QCoreApplication::applicationName().toLatin1() +
"_session";
50 const QVariantMap config = app->
engine()->
config(QLatin1String(
"Cutelyst_Session_Plugin"));
51 d->sessionExpires = config.value(QLatin1String(
"expires"), 7200).toLongLong();
52 d->expiryThreshold = config.value(QLatin1String(
"expiry_threshold"), 0).toLongLong();
53 d->verifyAddress = config.value(QLatin1String(
"verify_address"),
false).toBool();
54 d->verifyUserAgent = config.value(QLatin1String(
"verify_user_agent"),
false).toBool();
55 d->cookieHttpOnly = config.value(QLatin1String(
"cookie_http_only"),
true).toBool();
56 d->cookieSecure = config.value(QLatin1String(
"cookie_secure"),
false).toBool();
58 const QString _sameSite = config.value(u
"cookie_same_site"_qs, u
"strict"_qs).toString();
59 if (_sameSite.compare(u
"default", Qt::CaseInsensitive) == 0) {
60 d->cookieSameSite = QNetworkCookie::SameSite::Default;
61 }
else if (_sameSite.compare(u
"none", Qt::CaseInsensitive) == 0) {
62 d->cookieSameSite = QNetworkCookie::SameSite::None;
63 }
else if (_sameSite.compare(u
"lax", Qt::CaseInsensitive) == 0) {
64 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
66 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
73 d->store = std::make_unique<SessionStoreFile>(
this);
82 Q_ASSERT_X(d->store,
"Cutelyst::Session::setStorage",
"Session Storage is alread defined");
83 store->setParent(
this);
84 d->store = std::move(store);
90 return d->store.get();
96 const QVariant sid = c->
stash(SESSION_ID);
98 if (Q_UNLIKELY(!m_instance)) {
99 qCCritical(C_SESSION) <<
"Session plugin not registered";
103 ret = SessionPrivate::loadSessionId(c, m_instance->d_ptr->sessionName);
105 ret = sid.toByteArray();
118 if (Q_UNLIKELY(!m_instance)) {
119 qCCritical(C_SESSION) <<
"Session plugin not registered";
123 expires = SessionPrivate::loadSessionExpires(m_instance, c,
id(c));
125 return quint64(SessionPrivate::extendSessionExpires(m_instance, c,
expires.toLongLong()));
134 const qint64 timeExp = QDateTime::currentMSecsSinceEpoch() / 1000 + qint64(
expires);
136 if (Q_UNLIKELY(!m_instance)) {
137 qCCritical(C_SESSION) <<
"Session plugin not registered";
141 m_instance->d_ptr->store->storeSessionData(c, sid, u
"expires"_qs, timeExp);
146 if (Q_UNLIKELY(!m_instance)) {
147 qCCritical(C_SESSION) <<
"Session plugin not registered";
150 SessionPrivate::deleteSession(m_instance, c, reason);
155 return c->
stash(SESSION_DELETE_REASON).toString();
160 QVariant ret = defaultValue;
161 QVariant session = c->
stash(SESSION_VALUES);
162 if (session.isNull()) {
163 session = SessionPrivate::loadSession(c);
166 if (!session.isNull()) {
167 ret = session.toHash().value(key, defaultValue);
175 QVariant session = c->
stash(SESSION_VALUES);
176 if (session.isNull()) {
177 session = SessionPrivate::loadSession(c);
178 if (session.isNull()) {
179 if (Q_UNLIKELY(!m_instance)) {
180 qCCritical(C_SESSION) <<
"Session plugin not registered";
184 SessionPrivate::createSessionIdIfNeeded(
185 m_instance, c, m_instance->d_ptr->sessionExpires);
186 session = SessionPrivate::initializeSessionData(m_instance, c);
190 QVariantHash data = session.toHash();
191 data.insert(key,
value);
199 QVariant session = c->
stash(SESSION_VALUES);
200 if (session.isNull()) {
201 session = SessionPrivate::loadSession(c);
202 if (session.isNull()) {
203 if (Q_UNLIKELY(!m_instance)) {
204 qCCritical(C_SESSION) <<
"Session plugin not registered";
208 SessionPrivate::createSessionIdIfNeeded(
209 m_instance, c, m_instance->d_ptr->sessionExpires);
210 session = SessionPrivate::initializeSessionData(m_instance, c);
214 QVariantHash data = session.toHash();
223 QVariant session = c->
stash(SESSION_VALUES);
224 if (session.isNull()) {
225 session = SessionPrivate::loadSession(c);
226 if (session.isNull()) {
227 if (Q_UNLIKELY(!m_instance)) {
228 qCCritical(C_SESSION) <<
"Session plugin not registered";
232 SessionPrivate::createSessionIdIfNeeded(
233 m_instance, c, m_instance->d_ptr->sessionExpires);
234 session = SessionPrivate::initializeSessionData(m_instance, c);
238 QVariantHash data = session.toHash();
239 for (
const QString &key : keys) {
249 return !SessionPrivate::loadSession(c).isNull();
252QByteArray SessionPrivate::generateSessionId()
254 return QUuid::createUuid().toRfc4122().toHex();
257QByteArray SessionPrivate::loadSessionId(
Context *c,
const QByteArray &sessionName)
260 if (!c->
stash(SESSION_TRIED_LOADING_ID).isNull()) {
263 c->
setStash(SESSION_TRIED_LOADING_ID,
true);
265 const QByteArray sid = getSessionId(c, sessionName);
266 if (!sid.isEmpty()) {
267 if (!validateSessionId(sid)) {
268 qCCritical(C_SESSION) <<
"Tried to set invalid session ID" << sid;
278QByteArray SessionPrivate::getSessionId(
Context *c,
const QByteArray &sessionName)
281 bool deleted = !c->
stash(SESSION_DELETED_ID).isNull();
284 const QVariant
property = c->
stash(SESSION_ID);
285 if (!property.isNull()) {
286 ret =
property.toByteArray();
290 const QByteArray cookie = c->request()->
cookie(sessionName);
291 if (!cookie.isEmpty()) {
292 qCDebug(C_SESSION) <<
"Found sessionid" << cookie <<
"in cookie";
300QByteArray SessionPrivate::createSessionIdIfNeeded(
Session *session,
Context *c, qint64 expires)
303 const QVariant sid = c->
stash(SESSION_ID);
305 ret = sid.toByteArray();
307 ret = createSessionId(session, c, expires);
312QByteArray SessionPrivate::createSessionId(
Session *session,
Context *c, qint64 expires)
315 const auto sid = generateSessionId();
317 qCDebug(C_SESSION) <<
"Created session" << sid;
320 resetSessionExpires(session, c, sid);
321 setSessionId(session, c, sid);
326void SessionPrivate::_q_saveSession(
Context *c)
329 saveSessionExpires(c);
337 if (Q_UNLIKELY(!m_instance)) {
338 qCCritical(C_SESSION) <<
"Session plugin not registered";
341 saveSessionExpires(c);
343 if (!c->
stash(SESSION_UPDATED).toBool()) {
346 QVariantHash sessionData = c->
stash(SESSION_VALUES).toHash();
347 sessionData.insert(QStringLiteral(
"__updated"), QDateTime::currentMSecsSinceEpoch() / 1000);
349 const auto sid = c->
stash(SESSION_ID).toByteArray();
350 m_instance->d_ptr->store->storeSessionData(c, sid, QStringLiteral(
"session"), sessionData);
353void SessionPrivate::deleteSession(
Session *session,
Context *c,
const QString &reason)
355 qCDebug(C_SESSION) <<
"Deleting session" << reason;
357 const QVariant sidVar = c->
stash(SESSION_ID).toString();
358 if (!sidVar.isNull()) {
359 const auto sid = sidVar.toByteArray();
360 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral(
"session"));
361 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral(
"expires"));
362 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral(
"flash"));
364 deleteSessionId(session, c, sid);
368 c->
setStash(SESSION_VALUES, QVariant());
369 c->
setStash(SESSION_ID, QVariant());
370 c->
setStash(SESSION_EXPIRES, QVariant());
372 c->
setStash(SESSION_DELETE_REASON, reason);
375void SessionPrivate::deleteSessionId(
Session *session,
Context *c,
const QByteArray &sid)
377 c->
setStash(SESSION_DELETED_ID,
true);
379 updateSessionCookie(c, makeSessionCookie(session, c, sid, QDateTime::currentDateTimeUtc()));
382QVariant SessionPrivate::loadSession(
Context *c)
385 const QVariant
property = c->
stash(SESSION_VALUES);
386 if (!property.isNull()) {
387 ret =
property.toHash();
391 if (Q_UNLIKELY(!m_instance)) {
392 qCCritical(C_SESSION) <<
"Session plugin not registered";
397 if (!loadSessionExpires(m_instance, c, sid).isNull()) {
398 if (SessionPrivate::validateSessionId(sid)) {
400 const QVariantHash sessionData =
401 m_instance->d_ptr->store->getSessionData(c, sid, QStringLiteral(
"session"))
403 c->
setStash(SESSION_VALUES, sessionData);
405 if (m_instance->d_ptr->verifyAddress &&
406 sessionData.contains(QStringLiteral(
"__address")) &&
407 sessionData.
value(QStringLiteral(
"__address")).toString() !=
408 c->request()->
address().toString()) {
409 qCWarning(C_SESSION) <<
"Deleting session" << sid <<
"due to address mismatch:"
410 << sessionData.value(QStringLiteral(
"__address")).toString()
411 <<
"!=" << c->request()->
address().toString();
412 deleteSession(m_instance, c, QStringLiteral(
"address mismatch"));
416 if (m_instance->d_ptr->verifyUserAgent) {
417 auto it = sessionData.constFind(u
"__user_agent"_qs);
418 if (it != sessionData.constEnd() &&
419 it.value().toByteArray() != c->request()->userAgent()) {
421 <<
"Deleting session" << sid <<
"due to user agent mismatch:"
422 << sessionData.value(QStringLiteral(
"__user_agent")).toString()
423 <<
"!=" << c->request()->userAgent();
424 deleteSession(m_instance, c, QStringLiteral(
"user agent mismatch"));
429 qCDebug(C_SESSION) <<
"Restored session" << sid;
438bool SessionPrivate::validateSessionId(QByteArrayView
id)
440 auto it =
id.begin();
444 if ((c >=
'a' && c <=
'f') || (c >=
'0' && c <=
'9')) {
454qint64 SessionPrivate::extendSessionExpires(
Session *session,
Context *c, qint64 expires)
456 const qint64 threshold = qint64(session->d_ptr->expiryThreshold);
459 if (!sid.isEmpty()) {
460 const qint64 current = getStoredSessionExpires(session, c, sid);
461 const qint64 cutoff = current - threshold;
462 const qint64 time = QDateTime::currentMSecsSinceEpoch() / 1000;
464 if (!threshold || cutoff <= time || c->stash(SESSION_UPDATED).toBool()) {
465 qint64 updated = calculateInitialSessionExpires(session, c, sid);
466 c->
setStash(SESSION_EXTENDED_EXPIRES, updated);
467 extendSessionId(session, c, sid, updated);
478qint64 SessionPrivate::getStoredSessionExpires(
Session *session,
480 const QByteArray &sessionid)
482 const QVariant expires =
483 session->d_ptr->store->getSessionData(c, sessionid, QStringLiteral(
"expires"), 0);
484 return expires.toLongLong();
487QVariant SessionPrivate::initializeSessionData(
Session *session,
Context *c)
490 const qint64 now = QDateTime::currentMSecsSinceEpoch() / 1000;
491 ret.insert(QStringLiteral(
"__created"), now);
492 ret.insert(QStringLiteral(
"__updated"), now);
494 if (session->d_ptr->verifyAddress) {
495 ret.insert(QStringLiteral(
"__address"), c->request()->
address().toString());
498 if (session->d_ptr->verifyUserAgent) {
499 ret.insert(QStringLiteral(
"__user_agent"), c->request()->userAgent());
505void SessionPrivate::saveSessionExpires(
Context *c)
507 const QVariant expires = c->
stash(SESSION_EXPIRES);
508 if (!expires.isNull()) {
510 if (!sid.isEmpty()) {
511 if (Q_UNLIKELY(!m_instance)) {
512 qCCritical(C_SESSION) <<
"Session plugin not registered";
516 const qint64 current = getStoredSessionExpires(m_instance, c, sid);
518 if (extended > current) {
519 m_instance->d_ptr->store->storeSessionData(
520 c, sid, QStringLiteral(
"expires"), extended);
527 SessionPrivate::loadSessionExpires(
Session *session,
Context *c,
const QByteArray &sessionId)
530 if (c->
stash(SESSION_TRIED_LOADING_EXPIRES).toBool()) {
531 ret = c->
stash(SESSION_EXPIRES);
534 c->
setStash(SESSION_TRIED_LOADING_EXPIRES,
true);
536 if (!sessionId.isEmpty()) {
537 const qint64 expires = getStoredSessionExpires(session, c, sessionId);
539 if (expires >= QDateTime::currentMSecsSinceEpoch() / 1000) {
540 c->
setStash(SESSION_EXPIRES, expires);
543 deleteSession(session, c, QStringLiteral(
"session expired"));
550qint64 SessionPrivate::initialSessionExpires(
Session *session,
Context *c)
553 const qint64 expires = qint64(session->d_ptr->sessionExpires);
554 return QDateTime::currentMSecsSinceEpoch() / 1000 + expires;
557qint64 SessionPrivate::calculateInitialSessionExpires(
Session *session,
559 const QByteArray &sessionId)
561 const qint64 stored = getStoredSessionExpires(session, c, sessionId);
562 const qint64 initial = initialSessionExpires(session, c);
563 return qMax(initial, stored);
567 SessionPrivate::resetSessionExpires(
Session *session,
Context *c,
const QByteArray &sessionId)
569 const qint64 exp = calculateInitialSessionExpires(session, c, sessionId);
575 c->
setStash(SESSION_TRIED_LOADING_EXPIRES,
true);
576 c->
setStash(SESSION_EXTENDED_EXPIRES, exp);
581void SessionPrivate::updateSessionCookie(
Context *c,
const QNetworkCookie &updated)
586QNetworkCookie SessionPrivate::makeSessionCookie(
Session *session,
588 const QByteArray &sid,
589 const QDateTime &expires)
592 QNetworkCookie cookie(session->d_ptr->sessionName, sid);
593 cookie.setPath(u
"/"_qs);
594 cookie.setExpirationDate(expires);
595 cookie.setHttpOnly(session->d_ptr->cookieHttpOnly);
596 cookie.setSecure(session->d_ptr->cookieSecure);
597 cookie.setSameSitePolicy(session->d_ptr->cookieSameSite);
602void SessionPrivate::extendSessionId(
Session *session,
604 const QByteArray &sid,
608 c, makeSessionCookie(session, c, sid, QDateTime::fromMSecsSinceEpoch(expires * 1000)));
611void SessionPrivate::setSessionId(
Session *session,
Context *c,
const QByteArray &sid)
613 updateSessionCookie(c,
614 makeSessionCookie(session,
617 QDateTime::fromMSecsSinceEpoch(
618 initialSessionExpires(session, c) * 1000)));
626#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
user configuration for the application
QByteArray cookie(QByteArrayView name) const
QHostAddress address() const noexcept
void setCookie(const QNetworkCookie &cookie)
SessionStore(QObject *parent=nullptr)
static void deleteSession(Context *c, const QString &reason=QString())
static QString deleteReason(Context *c)
virtual bool setup(Application *app) final
Session(Application *parent)
static bool isValid(Context *c)
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 void changeExpires(Context *c, quint64 expires)
static QByteArray id(Context *c)
SessionStore * storage() const
static void deleteValue(Context *c, const QString &key)
static quint64 expires(Context *c)
static void deleteValues(Context *c, const QStringList &keys)
The Cutelyst namespace holds all public Cutelyst API.