cutelyst 5.0.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
session.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2013-2022 Daniel Nicoletti <dantti12@gmail.com>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5#include "session_p.h"
6#include "sessionstorefile.h"
7#include "utils.h"
8
9#include <Cutelyst/Application>
10#include <Cutelyst/Context>
11#include <Cutelyst/Engine>
12#include <Cutelyst/Response>
13
14#include <QCoreApplication>
15#include <QHostAddress>
16#include <QLoggingCategory>
17#include <QUuid>
18
19using namespace Cutelyst;
20using namespace Qt::Literals::StringLiterals;
21
22Q_LOGGING_CATEGORY(C_SESSION, "cutelyst.plugin.session", QtWarningMsg)
23
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
33
34namespace {
35thread_local Session *m_instance = nullptr;
36} // namespace
37
39 : Plugin(parent)
40 , d_ptr(new SessionPrivate(this))
41{
42}
43
44Session::Session(Cutelyst::Application *parent, const QVariantMap &defaultConfig)
45 : Plugin(parent)
46 , d_ptr(new SessionPrivate(this))
47{
48 d_ptr->defaultConfig = defaultConfig;
49}
50
52{
53 delete d_ptr;
54}
55
57{
58 Q_D(Session);
59 d->sessionName = QCoreApplication::applicationName().toLatin1() + "_session";
60
61 d->loadedConfig = app->engine()->config(u"Cutelyst_Session_Plugin"_s);
62 d->sessionExpires = std::chrono::duration_cast<std::chrono::seconds>(
63 Utils::durationFromString(d->config(u"expires"_s, 7200).toString()))
64 .count();
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();
70
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;
78 } else {
79 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
80 }
81
82 connect(app, &Application::afterDispatch, this, &SessionPrivate::_q_saveSession);
83 connect(app, &Application::postForked, this, [this] { m_instance = this; });
84
85 if (!d->store) {
86 d->store = std::make_unique<SessionStoreFile>(this);
87 }
88
89 return true;
90}
91
92void Session::setStorage(std::unique_ptr<Cutelyst::SessionStore> store)
93{
94 Q_D(Session);
95 Q_ASSERT_X(d->store, "Cutelyst::Session::setStorage", "Session Storage is alread defined");
96 store->setParent(this);
97 d->store = std::move(store);
98}
99
101{
102 Q_D(const Session);
103 return d->store.get();
104}
105
107{
108 QByteArray ret;
109 const QVariant sid = c->stash(SESSION_ID);
110 if (sid.isNull()) {
111 if (Q_UNLIKELY(!m_instance)) {
112 qCCritical(C_SESSION) << "Session plugin not registered";
113 return ret;
114 }
115
116 ret = SessionPrivate::loadSessionId(c, m_instance->d_ptr->sessionName);
117 } else {
118 ret = sid.toByteArray();
119 }
120
121 return ret;
122}
123
125{
126 QVariant expires = c->stash(SESSION_EXTENDED_EXPIRES);
127 if (!expires.isNull()) {
128 return expires.toLongLong();
129 }
130
131 if (Q_UNLIKELY(!m_instance)) {
132 qCCritical(C_SESSION) << "Session plugin not registered";
133 return 0;
134 }
135
136 expires = SessionPrivate::loadSessionExpires(m_instance, c, id(c));
137 if (!expires.isNull()) {
138 return SessionPrivate::extendSessionExpires(m_instance, c, expires.toLongLong());
139 }
140
141 return 0;
142}
143
144void Session::changeExpires(Context *c, qint64 expires)
145{
146 const QByteArray sid = Session::id(c);
147 const qint64 timeExp = QDateTime::currentSecsSinceEpoch() + expires;
148
149 if (Q_UNLIKELY(!m_instance)) {
150 qCCritical(C_SESSION) << "Session plugin not registered";
151 return;
152 }
153
154 m_instance->d_ptr->store->storeSessionData(c, sid, u"expires"_s, timeExp);
155}
156
158{
159 if (Q_UNLIKELY(!m_instance)) {
160 qCCritical(C_SESSION) << "Session plugin not registered";
161 return;
162 }
163 SessionPrivate::deleteSession(m_instance, c, reason);
164}
165
167{
168 return c->stash(SESSION_DELETE_REASON).toString();
169}
170
171QVariant Session::value(Cutelyst::Context *c, const QString &key, const QVariant &defaultValue)
172{
173 QVariant ret = defaultValue;
174 QVariant session = c->stash(SESSION_VALUES);
175 if (session.isNull()) {
176 session = SessionPrivate::loadSession(c);
177 }
178
179 if (!session.isNull()) {
180 ret = session.toHash().value(key, defaultValue);
181 }
182
183 return ret;
184}
185
186void Session::setValue(Cutelyst::Context *c, const QString &key, const QVariant &value)
187{
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";
194 return;
195 }
196
197 SessionPrivate::createSessionIdIfNeeded(
198 m_instance, c, m_instance->d_ptr->sessionExpires);
199 session = SessionPrivate::initializeSessionData(m_instance, c);
200 }
201 }
202
203 QVariantHash data = session.toHash();
204 data.insert(key, value);
205
206 c->setStash(SESSION_VALUES, data);
207 c->setStash(SESSION_UPDATED, true);
208}
209
211{
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";
218 return;
219 }
220
221 SessionPrivate::createSessionIdIfNeeded(
222 m_instance, c, m_instance->d_ptr->sessionExpires);
223 session = SessionPrivate::initializeSessionData(m_instance, c);
224 }
225 }
226
227 QVariantHash data = session.toHash();
228 data.remove(key);
229
230 c->setStash(SESSION_VALUES, data);
231 c->setStash(SESSION_UPDATED, true);
232}
233
235{
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";
242 return;
243 }
244
245 SessionPrivate::createSessionIdIfNeeded(
246 m_instance, c, m_instance->d_ptr->sessionExpires);
247 session = SessionPrivate::initializeSessionData(m_instance, c);
248 }
249 }
250
251 QVariantHash data = session.toHash();
252 for (const QString &key : keys) {
253 data.remove(key);
254 }
255
256 c->setStash(SESSION_VALUES, data);
257 c->setStash(SESSION_UPDATED, true);
258}
259
261{
262 return !SessionPrivate::loadSession(c).isNull();
263}
264
265QByteArray SessionPrivate::generateSessionId()
266{
267 return QUuid::createUuid().toRfc4122().toHex();
268}
269
270QByteArray SessionPrivate::loadSessionId(Context *c, const QByteArray &sessionName)
271{
272 QByteArray ret;
273 if (!c->stash(SESSION_TRIED_LOADING_ID).isNull()) {
274 return ret;
275 }
276 c->setStash(SESSION_TRIED_LOADING_ID, true);
277
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;
282 return ret;
283 }
284 ret = sid;
285 c->setStash(SESSION_ID, sid);
286 }
287
288 return ret;
289}
290
291QByteArray SessionPrivate::getSessionId(const Context *c, const QByteArray &sessionName)
292{
293 QByteArray ret;
294 bool deleted = !c->stash(SESSION_DELETED_ID).isNull();
295
296 if (!deleted) {
297 const QVariant property = c->stash(SESSION_ID);
298 if (!property.isNull()) {
299 ret = property.toByteArray();
300 return ret;
301 }
302
303 const QByteArray cookie = c->request()->cookie(sessionName);
304 if (!cookie.isEmpty()) {
305 qCDebug(C_SESSION) << "Found sessionid" << cookie << "in cookie";
306 ret = cookie;
307 }
308 }
309
310 return ret;
311}
312
313QByteArray SessionPrivate::createSessionIdIfNeeded(Session *session, Context *c, qint64 expires)
314{
315 QByteArray ret;
316 const QVariant sid = c->stash(SESSION_ID);
317 if (!sid.isNull()) {
318 ret = sid.toByteArray();
319 } else {
320 ret = createSessionId(session, c, expires);
321 }
322 return ret;
323}
324
325QByteArray SessionPrivate::createSessionId(Session *session, Context *c, qint64 expires)
326{
327 Q_UNUSED(expires)
328 const auto sid = generateSessionId();
329
330 qCDebug(C_SESSION) << "Created session" << sid;
331
332 c->setStash(SESSION_ID, sid);
333 resetSessionExpires(session, c, sid);
334 setSessionId(session, c, sid);
335
336 return sid;
337}
338
339void SessionPrivate::_q_saveSession(Context *c)
340{
341 // fix cookie before we send headers
342 saveSessionExpires(c);
343
344 // Force extension of session_expires before finalizing headers, so a pos
345 // up to date. First call to session_expires will extend the expiry, methods
346 // just return the previously extended value.
348
349 // Persist data
350 if (Q_UNLIKELY(!m_instance)) {
351 qCCritical(C_SESSION) << "Session plugin not registered";
352 return;
353 }
354 saveSessionExpires(c);
355
356 if (!c->stash(SESSION_UPDATED).toBool()) {
357 return;
358 }
359 QVariantHash sessionData = c->stash(SESSION_VALUES).toHash();
360 sessionData.insert(u"__updated"_s, QDateTime::currentSecsSinceEpoch());
361
362 const auto sid = c->stash(SESSION_ID).toByteArray();
363 m_instance->d_ptr->store->storeSessionData(c, sid, u"session"_s, sessionData);
364}
365
366void SessionPrivate::deleteSession(Session *session, Context *c, const QString &reason)
367{
368 qCDebug(C_SESSION) << "Deleting session" << reason;
369
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);
376
377 deleteSessionId(session, c, sid);
378 }
379
380 // Reset the values in Context object
381 c->setStash(SESSION_VALUES, QVariant());
382 c->setStash(SESSION_ID, QVariant());
383 c->setStash(SESSION_EXPIRES, QVariant());
384
385 c->setStash(SESSION_DELETE_REASON, reason);
386}
387
388void SessionPrivate::deleteSessionId(Session *session, Context *c, const QByteArray &sid)
389{
390 c->setStash(SESSION_DELETED_ID, true); // to prevent get_session_id from returning it
391
392 updateSessionCookie(c, makeSessionCookie(session, c, sid, QDateTime::currentDateTimeUtc()));
393}
394
395QVariant SessionPrivate::loadSession(Context *c)
396{
397 QVariant ret;
398 const QVariant property = c->stash(SESSION_VALUES);
399 if (!property.isNull()) {
400 ret = property.toHash();
401 return ret;
402 }
403
404 if (Q_UNLIKELY(!m_instance)) {
405 qCCritical(C_SESSION) << "Session plugin not registered";
406 return ret;
407 }
408
409 const auto sid = Session::id(c);
410 if (!loadSessionExpires(m_instance, c, sid).isNull()) {
411 if (SessionPrivate::validateSessionId(sid)) {
412
413 const QVariantHash sessionData =
414 m_instance->d_ptr->store->getSessionData(c, sid, u"session"_s).toHash();
415 c->setStash(SESSION_VALUES, sessionData);
416
417 if (m_instance->d_ptr->verifyAddress) {
418 auto it = sessionData.constFind(u"__address"_s);
419 if (it != sessionData.constEnd() &&
420 it->toString() != c->request()->address().toString()) {
421 qCWarning(C_SESSION)
422 << "Deleting session" << sid << "due to address mismatch:" << *it
423 << "!=" << c->request()->address().toString();
424 deleteSession(m_instance, c, u"address mismatch"_s);
425 return ret;
426 }
427 }
428
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()) {
433 qCWarning(C_SESSION)
434 << "Deleting session" << sid << "due to user agent mismatch:" << *it
435 << "!=" << c->request()->userAgent();
436 deleteSession(m_instance, c, u"user agent mismatch"_s);
437 return ret;
438 }
439 }
440
441 qCDebug(C_SESSION) << "Restored session" << sid << "keys" << sessionData.size();
442
443 ret = sessionData;
444 }
445 }
446
447 return ret;
448}
449
450bool SessionPrivate::validateSessionId(QByteArrayView id)
451{
452 return !id.empty() && std::ranges::all_of(id, [](char c) {
453 return (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
454 });
455}
456
457qint64 SessionPrivate::extendSessionExpires(Session *session, Context *c, qint64 expires)
458{
459 const qint64 threshold = session->d_ptr->expiryThreshold;
460
461 const auto sid = Session::id(c);
462 if (!sid.isEmpty()) {
463 const qint64 current = getStoredSessionExpires(session, c, sid);
464 const qint64 cutoff = current - threshold;
465 const qint64 time = QDateTime::currentSecsSinceEpoch();
466
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);
471
472 return updated;
473 } else {
474 return current;
475 }
476 } else {
477 return expires;
478 }
479}
480
481qint64 SessionPrivate::getStoredSessionExpires(Session *session,
482 Context *c,
483 const QByteArray &sessionid)
484{
485 const QVariant expires = session->d_ptr->store->getSessionData(c, sessionid, u"expires"_s, 0);
486 return expires.toLongLong();
487}
488
489QVariant SessionPrivate::initializeSessionData(const Session *session, const Context *c)
490{
491 QVariantHash ret;
492 const qint64 now = QDateTime::currentSecsSinceEpoch();
493 ret.insert(u"__created"_s, now);
494 ret.insert(u"__updated"_s, now);
495
496 if (session->d_ptr->verifyAddress) {
497 ret.insert(u"__address"_s, c->request()->address().toString());
498 }
499
500 if (session->d_ptr->verifyUserAgent) {
501 ret.insert(u"__user_agent"_s, c->request()->userAgent());
502 }
503
504 return ret;
505}
506
507void SessionPrivate::saveSessionExpires(Context *c)
508{
509 const QVariant expires = c->stash(SESSION_EXPIRES);
510 if (!expires.isNull()) {
511 const auto sid = Session::id(c);
512 if (!sid.isEmpty()) {
513 if (Q_UNLIKELY(!m_instance)) {
514 qCCritical(C_SESSION) << "Session plugin not registered";
515 return;
516 }
517
518 const qint64 current = getStoredSessionExpires(m_instance, c, sid);
519 const qint64 extended = Session::expires(c);
520 if (extended > current) {
521 m_instance->d_ptr->store->storeSessionData(c, sid, u"expires"_s, extended);
522 }
523 }
524 }
525}
526
528 SessionPrivate::loadSessionExpires(Session *session, Context *c, const QByteArray &sessionId)
529{
530 QVariant ret;
531 if (c->stash(SESSION_TRIED_LOADING_EXPIRES).toBool()) {
532 ret = c->stash(SESSION_EXPIRES);
533 return ret;
534 }
535 c->setStash(SESSION_TRIED_LOADING_EXPIRES, true);
536
537 if (!sessionId.isEmpty()) {
538 const qint64 expires = getStoredSessionExpires(session, c, sessionId);
539
540 if (expires >= QDateTime::currentSecsSinceEpoch()) {
541 c->setStash(SESSION_EXPIRES, expires);
542 ret = expires;
543 } else {
544 deleteSession(session, c, u"session expired"_s);
545 ret = 0;
546 }
547 }
548 return ret;
549}
550
551qint64 SessionPrivate::initialSessionExpires(Session *session, Context *c)
552{
553 Q_UNUSED(c)
554 const qint64 expires = session->d_ptr->sessionExpires;
555 return QDateTime::currentSecsSinceEpoch() + expires;
556}
557
558qint64 SessionPrivate::calculateInitialSessionExpires(Session *session,
559 Context *c,
560 const QByteArray &sessionId)
561{
562 const qint64 stored = getStoredSessionExpires(session, c, sessionId);
563 const qint64 initial = initialSessionExpires(session, c);
564 return qMax(initial, stored);
565}
566
567qint64
568 SessionPrivate::resetSessionExpires(Session *session, Context *c, const QByteArray &sessionId)
569{
570 const qint64 exp = calculateInitialSessionExpires(session, c, sessionId);
571
572 c->setStash(SESSION_EXPIRES, exp);
573
574 // since we're setting _session_expires directly, make loadSessionExpires
575 // actually use that value.
576 c->setStash(SESSION_TRIED_LOADING_EXPIRES, true);
577 c->setStash(SESSION_EXTENDED_EXPIRES, exp);
578
579 return exp;
580}
581
582void SessionPrivate::updateSessionCookie(const Context *c, const QNetworkCookie &updated)
583{
584 c->response()->setCookie(updated);
585}
586
587QNetworkCookie SessionPrivate::makeSessionCookie(Session *session,
588 const Context *c,
589 const QByteArray &sid,
590 const QDateTime &expires)
591{
592 Q_UNUSED(c)
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);
599
600 return cookie;
601}
602
603void SessionPrivate::extendSessionId(Session *session,
604 const Context *c,
605 const QByteArray &sid,
606 qint64 expires)
607{
608 updateSessionCookie(c,
609 makeSessionCookie(session, c, sid, QDateTime::fromSecsSinceEpoch(expires)));
610}
611
612void SessionPrivate::setSessionId(Session *session, Context *c, const QByteArray &sid)
613{
614 updateSessionCookie(
615 c,
616 makeSessionCookie(
617 session, c, sid, QDateTime::fromSecsSinceEpoch(initialSessionExpires(session, c))));
618}
619
620QVariant SessionPrivate::config(const QString &key, const QVariant &defaultValue) const
621{
622 return loadedConfig.value(key, defaultConfig.value(key, defaultValue));
623}
624
626 : QObject(parent)
627{
628}
629
630#include "moc_session.cpp"
The Cutelyst application.
Definition application.h:66
Engine * engine() const noexcept
void afterDispatch(Cutelyst::Context *c)
void postForked(Cutelyst::Application *app)
The Cutelyst Context.
Definition context.h:42
void stash(const QVariantHash &unite)
Definition context.cpp:562
Request * request
Definition context.h:71
void setStash(const QString &key, const QVariant &value)
Definition context.cpp:213
Response * response() const noexcept
Definition context.cpp:98
QVariantMap config(const QString &entity) const
Definition engine.cpp:122
Base class for Cutelyst Plugins.
Definition plugin.h:25
QByteArray cookie(QAnyStringView name) const
Definition request.cpp:277
QHostAddress address() const noexcept
Definition request.cpp:34
void setCookie(const QNetworkCookie &cookie)
Definition response.cpp:202
Abstract class to create a session store.
Definition session.h:36
SessionStore(QObject *parent=nullptr)
Definition session.cpp:625
Plugin providing methods for session management.
Definition session.h:161
static qint64 expires(Context *c)
Definition session.cpp:124
static QString deleteReason(Context *c)
Definition session.cpp:166
virtual bool setup(Application *app) final
Definition session.cpp:56
Session(Application *parent)
Definition session.cpp:38
static bool isValid(Context *c)
Definition session.cpp:260
static void deleteSession(Context *c, const QString &reason={})
Definition session.cpp:157
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition session.cpp:171
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition session.cpp:186
void setStorage(std::unique_ptr< SessionStore > store)
Definition session.cpp:92
static QByteArray id(Context *c)
Definition session.cpp:106
SessionStore * storage() const
Definition session.cpp:100
static void deleteValue(Context *c, const QString &key)
Definition session.cpp:210
virtual ~Session()
Definition session.cpp:51
static void deleteValues(Context *c, const QStringList &keys)
Definition session.cpp:234
static void changeExpires(Context *c, qint64 expires)
Definition session.cpp:144
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition utils.cpp:302
The Cutelyst namespace holds all public Cutelyst API.
bool isEmpty() const const
qsizetype size() const const
QByteArray toHex(char separator) const const
QDateTime currentDateTimeUtc()
qint64 currentSecsSinceEpoch()
QDateTime fromSecsSinceEpoch(qint64 secs)
QString toString() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
CaseInsensitive
QUuid createUuid()
QByteArray toRfc4122() const const
bool isNull() const const
QByteArray toByteArray() const const
QHash< QString, QVariant > toHash() const const
qlonglong toLongLong(bool *ok) const const
T value() const const