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")
34static thread_local Session *m_instance =
nullptr;
38 , d_ptr(new SessionPrivate(this))
44 , d_ptr(new SessionPrivate(this))
46 d_ptr->defaultConfig = defaultConfig;
57 d->sessionName = QCoreApplication::applicationName().toLatin1() +
"_session";
59 d->loadedConfig = app->
engine()->
config(u
"Cutelyst_Session_Plugin"_s);
60 d->sessionExpires = std::chrono::duration_cast<std::chrono::seconds>(
63 d->expiryThreshold = d->config(u
"expiry_threshold"_s, 0).toLongLong();
64 d->verifyAddress = d->config(u
"verify_address"_s,
false).toBool();
65 d->verifyUserAgent = d->config(u
"verify_user_agent"_s,
false).toBool();
66 d->cookieHttpOnly = d->config(u
"cookie_http_only"_s,
true).toBool();
67 d->cookieSecure = d->config(u
"cookie_secure"_s,
false).toBool();
69 const QString _sameSite = d->config(u
"cookie_same_site"_s, u
"strict"_s).toString();
70 if (_sameSite.compare(u
"default", Qt::CaseInsensitive) == 0) {
71 d->cookieSameSite = QNetworkCookie::SameSite::Default;
72 }
else if (_sameSite.compare(u
"none", Qt::CaseInsensitive) == 0) {
73 d->cookieSameSite = QNetworkCookie::SameSite::None;
74 }
else if (_sameSite.compare(u
"lax", Qt::CaseInsensitive) == 0) {
75 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
77 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
84 d->store = std::make_unique<SessionStoreFile>(
this);
93 Q_ASSERT_X(d->store,
"Cutelyst::Session::setStorage",
"Session Storage is alread defined");
94 store->setParent(
this);
95 d->store = std::move(store);
101 return d->store.get();
107 const QVariant sid = c->
stash(SESSION_ID);
109 if (Q_UNLIKELY(!m_instance)) {
110 qCCritical(C_SESSION) <<
"Session plugin not registered";
114 ret = SessionPrivate::loadSessionId(c, m_instance->d_ptr->sessionName);
116 ret = sid.toByteArray();
129 if (Q_UNLIKELY(!m_instance)) {
130 qCCritical(C_SESSION) <<
"Session plugin not registered";
134 expires = SessionPrivate::loadSessionExpires(m_instance, c,
id(c));
136 return quint64(SessionPrivate::extendSessionExpires(m_instance, c,
expires.toLongLong()));
145 const qint64 timeExp = QDateTime::currentSecsSinceEpoch() + qint64(
expires);
147 if (Q_UNLIKELY(!m_instance)) {
148 qCCritical(C_SESSION) <<
"Session plugin not registered";
152 m_instance->d_ptr->store->storeSessionData(c, sid, u
"expires"_s, timeExp);
157 if (Q_UNLIKELY(!m_instance)) {
158 qCCritical(C_SESSION) <<
"Session plugin not registered";
161 SessionPrivate::deleteSession(m_instance, c, reason);
166 return c->
stash(SESSION_DELETE_REASON).toString();
171 QVariant ret = defaultValue;
172 QVariant session = c->
stash(SESSION_VALUES);
173 if (session.isNull()) {
174 session = SessionPrivate::loadSession(c);
177 if (!session.isNull()) {
178 ret = session.toHash().value(key, defaultValue);
186 QVariant session = c->
stash(SESSION_VALUES);
187 if (session.isNull()) {
188 session = SessionPrivate::loadSession(c);
189 if (session.isNull()) {
190 if (Q_UNLIKELY(!m_instance)) {
191 qCCritical(C_SESSION) <<
"Session plugin not registered";
195 SessionPrivate::createSessionIdIfNeeded(
196 m_instance, c, m_instance->d_ptr->sessionExpires);
197 session = SessionPrivate::initializeSessionData(m_instance, c);
201 QVariantHash data = session.toHash();
202 data.insert(key,
value);
210 QVariant session = c->
stash(SESSION_VALUES);
211 if (session.isNull()) {
212 session = SessionPrivate::loadSession(c);
213 if (session.isNull()) {
214 if (Q_UNLIKELY(!m_instance)) {
215 qCCritical(C_SESSION) <<
"Session plugin not registered";
219 SessionPrivate::createSessionIdIfNeeded(
220 m_instance, c, m_instance->d_ptr->sessionExpires);
221 session = SessionPrivate::initializeSessionData(m_instance, c);
225 QVariantHash data = session.toHash();
234 QVariant session = c->
stash(SESSION_VALUES);
235 if (session.isNull()) {
236 session = SessionPrivate::loadSession(c);
237 if (session.isNull()) {
238 if (Q_UNLIKELY(!m_instance)) {
239 qCCritical(C_SESSION) <<
"Session plugin not registered";
243 SessionPrivate::createSessionIdIfNeeded(
244 m_instance, c, m_instance->d_ptr->sessionExpires);
245 session = SessionPrivate::initializeSessionData(m_instance, c);
249 QVariantHash data = session.toHash();
250 for (
const QString &key : keys) {
260 return !SessionPrivate::loadSession(c).isNull();
263QByteArray SessionPrivate::generateSessionId()
265 return QUuid::createUuid().toRfc4122().toHex();
268QByteArray SessionPrivate::loadSessionId(
Context *c,
const QByteArray &sessionName)
271 if (!c->
stash(SESSION_TRIED_LOADING_ID).isNull()) {
274 c->
setStash(SESSION_TRIED_LOADING_ID,
true);
276 const QByteArray sid = getSessionId(c, sessionName);
277 if (!sid.isEmpty()) {
278 if (!validateSessionId(sid)) {
279 qCCritical(C_SESSION) <<
"Tried to set invalid session ID" << sid;
289QByteArray SessionPrivate::getSessionId(
Context *c,
const QByteArray &sessionName)
292 bool deleted = !c->
stash(SESSION_DELETED_ID).isNull();
295 const QVariant
property = c->
stash(SESSION_ID);
296 if (!property.isNull()) {
297 ret =
property.toByteArray();
302 if (!cookie.isEmpty()) {
303 qCDebug(C_SESSION) <<
"Found sessionid" << cookie <<
"in cookie";
311QByteArray SessionPrivate::createSessionIdIfNeeded(
Session *session,
Context *c, qint64 expires)
314 const QVariant sid = c->
stash(SESSION_ID);
316 ret = sid.toByteArray();
318 ret = createSessionId(session, c, expires);
323QByteArray SessionPrivate::createSessionId(
Session *session,
Context *c, qint64 expires)
326 const auto sid = generateSessionId();
328 qCDebug(C_SESSION) <<
"Created session" << sid;
331 resetSessionExpires(session, c, sid);
332 setSessionId(session, c, sid);
337void SessionPrivate::_q_saveSession(
Context *c)
340 saveSessionExpires(c);
348 if (Q_UNLIKELY(!m_instance)) {
349 qCCritical(C_SESSION) <<
"Session plugin not registered";
352 saveSessionExpires(c);
354 if (!c->
stash(SESSION_UPDATED).toBool()) {
357 QVariantHash sessionData = c->
stash(SESSION_VALUES).toHash();
358 sessionData.insert(QStringLiteral(
"__updated"), QDateTime::currentSecsSinceEpoch());
360 const auto sid = c->
stash(SESSION_ID).toByteArray();
361 m_instance->d_ptr->store->storeSessionData(c, sid, QStringLiteral(
"session"), sessionData);
364void SessionPrivate::deleteSession(
Session *session,
Context *c,
const QString &reason)
366 qCDebug(C_SESSION) <<
"Deleting session" << reason;
368 const QVariant sidVar = c->
stash(SESSION_ID).toString();
369 if (!sidVar.isNull()) {
370 const auto sid = sidVar.toByteArray();
371 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral(
"session"));
372 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral(
"expires"));
373 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral(
"flash"));
375 deleteSessionId(session, c, sid);
379 c->
setStash(SESSION_VALUES, QVariant());
380 c->
setStash(SESSION_ID, QVariant());
381 c->
setStash(SESSION_EXPIRES, QVariant());
383 c->
setStash(SESSION_DELETE_REASON, reason);
386void SessionPrivate::deleteSessionId(
Session *session,
Context *c,
const QByteArray &sid)
388 c->
setStash(SESSION_DELETED_ID,
true);
390 updateSessionCookie(c, makeSessionCookie(session, c, sid, QDateTime::currentDateTimeUtc()));
393QVariant SessionPrivate::loadSession(
Context *c)
396 const QVariant
property = c->
stash(SESSION_VALUES);
397 if (!property.isNull()) {
398 ret =
property.toHash();
402 if (Q_UNLIKELY(!m_instance)) {
403 qCCritical(C_SESSION) <<
"Session plugin not registered";
408 if (!loadSessionExpires(m_instance, c, sid).isNull()) {
409 if (SessionPrivate::validateSessionId(sid)) {
411 const QVariantHash sessionData =
412 m_instance->d_ptr->store->getSessionData(c, sid, QStringLiteral(
"session"))
414 c->
setStash(SESSION_VALUES, sessionData);
416 if (m_instance->d_ptr->verifyAddress) {
417 auto it = sessionData.constFind(u
"__address"_s);
418 if (it != sessionData.constEnd() &&
421 <<
"Deleting session" << sid <<
"due to address mismatch:" << *it
423 deleteSession(m_instance, c, QStringLiteral(
"address mismatch"));
428 if (m_instance->d_ptr->verifyUserAgent) {
429 auto it = sessionData.constFind(u
"__user_agent"_s);
430 if (it != sessionData.constEnd() &&
431 it->toByteArray() != c->
request()->userAgent()) {
433 <<
"Deleting session" << sid <<
"due to user agent mismatch:" << *it
434 <<
"!=" << c->
request()->userAgent();
435 deleteSession(m_instance, c, QStringLiteral(
"user agent mismatch"));
440 qCDebug(C_SESSION) <<
"Restored session" << sid <<
"keys" << sessionData.size();
449bool SessionPrivate::validateSessionId(QByteArrayView
id)
451 auto it =
id.begin();
455 if ((c >=
'a' && c <=
'f') || (c >=
'0' && c <=
'9')) {
465qint64 SessionPrivate::extendSessionExpires(
Session *session,
Context *c, qint64 expires)
467 const qint64 threshold = qint64(session->d_ptr->expiryThreshold);
470 if (!sid.isEmpty()) {
471 const qint64 current = getStoredSessionExpires(session, c, sid);
472 const qint64 cutoff = current - threshold;
473 const qint64 time = QDateTime::currentSecsSinceEpoch();
475 if (!threshold || cutoff <= time || c->stash(SESSION_UPDATED).toBool()) {
476 qint64 updated = calculateInitialSessionExpires(session, c, sid);
477 c->
setStash(SESSION_EXTENDED_EXPIRES, updated);
478 extendSessionId(session, c, sid, updated);
489qint64 SessionPrivate::getStoredSessionExpires(
Session *session,
491 const QByteArray &sessionid)
493 const QVariant expires =
494 session->d_ptr->store->getSessionData(c, sessionid, QStringLiteral(
"expires"), 0);
495 return expires.toLongLong();
498QVariant SessionPrivate::initializeSessionData(
Session *session,
Context *c)
501 const qint64 now = QDateTime::currentSecsSinceEpoch();
502 ret.insert(QStringLiteral(
"__created"), now);
503 ret.insert(QStringLiteral(
"__updated"), now);
505 if (session->d_ptr->verifyAddress) {
506 ret.insert(QStringLiteral(
"__address"), c->
request()->
address().toString());
509 if (session->d_ptr->verifyUserAgent) {
510 ret.insert(QStringLiteral(
"__user_agent"), c->
request()->userAgent());
516void SessionPrivate::saveSessionExpires(
Context *c)
518 const QVariant expires = c->
stash(SESSION_EXPIRES);
519 if (!expires.isNull()) {
521 if (!sid.isEmpty()) {
522 if (Q_UNLIKELY(!m_instance)) {
523 qCCritical(C_SESSION) <<
"Session plugin not registered";
527 const qint64 current = getStoredSessionExpires(m_instance, c, sid);
529 if (extended > current) {
530 m_instance->d_ptr->store->storeSessionData(
531 c, sid, QStringLiteral(
"expires"), extended);
538 SessionPrivate::loadSessionExpires(
Session *session,
Context *c,
const QByteArray &sessionId)
541 if (c->
stash(SESSION_TRIED_LOADING_EXPIRES).toBool()) {
542 ret = c->
stash(SESSION_EXPIRES);
545 c->
setStash(SESSION_TRIED_LOADING_EXPIRES,
true);
547 if (!sessionId.isEmpty()) {
548 const qint64 expires = getStoredSessionExpires(session, c, sessionId);
550 if (expires >= QDateTime::currentSecsSinceEpoch()) {
551 c->
setStash(SESSION_EXPIRES, expires);
554 deleteSession(session, c, QStringLiteral(
"session expired"));
561qint64 SessionPrivate::initialSessionExpires(
Session *session,
Context *c)
564 const qint64 expires = qint64(session->d_ptr->sessionExpires);
565 return QDateTime::currentSecsSinceEpoch() + expires;
568qint64 SessionPrivate::calculateInitialSessionExpires(
Session *session,
570 const QByteArray &sessionId)
572 const qint64 stored = getStoredSessionExpires(session, c, sessionId);
573 const qint64 initial = initialSessionExpires(session, c);
574 return qMax(initial, stored);
578 SessionPrivate::resetSessionExpires(
Session *session,
Context *c,
const QByteArray &sessionId)
580 const qint64 exp = calculateInitialSessionExpires(session, c, sessionId);
586 c->
setStash(SESSION_TRIED_LOADING_EXPIRES,
true);
587 c->
setStash(SESSION_EXTENDED_EXPIRES, exp);
592void SessionPrivate::updateSessionCookie(
Context *c,
const QNetworkCookie &updated)
597QNetworkCookie SessionPrivate::makeSessionCookie(
Session *session,
599 const QByteArray &sid,
600 const QDateTime &expires)
603 QNetworkCookie cookie(session->d_ptr->sessionName, sid);
604 cookie.setPath(u
"/"_s);
605 cookie.setExpirationDate(expires);
606 cookie.setHttpOnly(session->d_ptr->cookieHttpOnly);
607 cookie.setSecure(session->d_ptr->cookieSecure);
608 cookie.setSameSitePolicy(session->d_ptr->cookieSameSite);
613void SessionPrivate::extendSessionId(
Session *session,
615 const QByteArray &sid,
618 updateSessionCookie(c,
619 makeSessionCookie(session, c, sid, QDateTime::fromSecsSinceEpoch(expires)));
622void SessionPrivate::setSessionId(
Session *session,
Context *c,
const QByteArray &sid)
627 session, c, sid, QDateTime::fromSecsSinceEpoch(initialSessionExpires(session, c))));
630QVariant SessionPrivate::config(
const QString &key,
const QVariant &defaultValue)
const
632 return loadedConfig.value(key, defaultConfig.value(key, defaultValue));
640#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(QByteArrayView 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 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)
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
The Cutelyst namespace holds all public Cutelyst API.