cutelyst 4.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
8#include <Cutelyst/Application>
9#include <Cutelyst/Context>
10#include <Cutelyst/Engine>
11#include <Cutelyst/Response>
12
13#include <QCoreApplication>
14#include <QHostAddress>
15#include <QLoggingCategory>
16#include <QUuid>
17
18using namespace Cutelyst;
19
20Q_LOGGING_CATEGORY(C_SESSION, "cutelyst.plugin.session", QtWarningMsg)
21
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")
31
32static thread_local Session *m_instance = nullptr;
33
35 : Plugin(parent)
36 , d_ptr(new SessionPrivate(this))
37{
38}
39
40Cutelyst::Session::~Session()
41{
42 delete d_ptr;
43}
44
46{
47 Q_D(Session);
48 d->sessionName = QCoreApplication::applicationName().toLatin1() + "_session";
49
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();
57
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;
65 } else {
66 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
67 }
68
69 connect(app, &Application::afterDispatch, this, &SessionPrivate::_q_saveSession);
70 connect(app, &Application::postForked, this, [this] { m_instance = this; });
71
72 if (!d->store) {
73 d->store = std::make_unique<SessionStoreFile>(this);
74 }
75
76 return true;
77}
78
79void Session::setStorage(std::unique_ptr<Cutelyst::SessionStore> store)
80{
81 Q_D(Session);
82 Q_ASSERT_X(d->store, "Cutelyst::Session::setStorage", "Session Storage is alread defined");
83 store->setParent(this);
84 d->store = std::move(store);
85}
86
88{
89 Q_D(const Session);
90 return d->store.get();
91}
92
94{
95 QByteArray ret;
96 const QVariant sid = c->stash(SESSION_ID);
97 if (sid.isNull()) {
98 if (Q_UNLIKELY(!m_instance)) {
99 qCCritical(C_SESSION) << "Session plugin not registered";
100 return ret;
101 }
102
103 ret = SessionPrivate::loadSessionId(c, m_instance->d_ptr->sessionName);
104 } else {
105 ret = sid.toByteArray();
106 }
107
108 return ret;
109}
110
112{
113 QVariant expires = c->stash(SESSION_EXTENDED_EXPIRES);
114 if (!expires.isNull()) {
115 return expires.toULongLong();
116 }
117
118 if (Q_UNLIKELY(!m_instance)) {
119 qCCritical(C_SESSION) << "Session plugin not registered";
120 return 0;
121 }
122
123 expires = SessionPrivate::loadSessionExpires(m_instance, c, id(c));
124 if (!expires.isNull()) {
125 return quint64(SessionPrivate::extendSessionExpires(m_instance, c, expires.toLongLong()));
126 }
127
128 return 0;
129}
130
131void Session::changeExpires(Context *c, quint64 expires)
132{
133 const QByteArray sid = Session::id(c);
134 const qint64 timeExp = QDateTime::currentMSecsSinceEpoch() / 1000 + qint64(expires);
135
136 if (Q_UNLIKELY(!m_instance)) {
137 qCCritical(C_SESSION) << "Session plugin not registered";
138 return;
139 }
140
141 m_instance->d_ptr->store->storeSessionData(c, sid, u"expires"_qs, timeExp);
142}
143
144void Session::deleteSession(Context *c, const QString &reason)
145{
146 if (Q_UNLIKELY(!m_instance)) {
147 qCCritical(C_SESSION) << "Session plugin not registered";
148 return;
149 }
150 SessionPrivate::deleteSession(m_instance, c, reason);
151}
152
154{
155 return c->stash(SESSION_DELETE_REASON).toString();
156}
157
158QVariant Session::value(Cutelyst::Context *c, const QString &key, const QVariant &defaultValue)
159{
160 QVariant ret = defaultValue;
161 QVariant session = c->stash(SESSION_VALUES);
162 if (session.isNull()) {
163 session = SessionPrivate::loadSession(c);
164 }
165
166 if (!session.isNull()) {
167 ret = session.toHash().value(key, defaultValue);
168 }
169
170 return ret;
171}
172
173void Session::setValue(Cutelyst::Context *c, const QString &key, const QVariant &value)
174{
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";
181 return;
182 }
183
184 SessionPrivate::createSessionIdIfNeeded(
185 m_instance, c, m_instance->d_ptr->sessionExpires);
186 session = SessionPrivate::initializeSessionData(m_instance, c);
187 }
188 }
189
190 QVariantHash data = session.toHash();
191 data.insert(key, value);
192
193 c->setStash(SESSION_VALUES, data);
194 c->setStash(SESSION_UPDATED, true);
195}
196
197void Session::deleteValue(Context *c, const QString &key)
198{
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";
205 return;
206 }
207
208 SessionPrivate::createSessionIdIfNeeded(
209 m_instance, c, m_instance->d_ptr->sessionExpires);
210 session = SessionPrivate::initializeSessionData(m_instance, c);
211 }
212 }
213
214 QVariantHash data = session.toHash();
215 data.remove(key);
216
217 c->setStash(SESSION_VALUES, data);
218 c->setStash(SESSION_UPDATED, true);
219}
220
221void Session::deleteValues(Context *c, const QStringList &keys)
222{
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";
229 return;
230 }
231
232 SessionPrivate::createSessionIdIfNeeded(
233 m_instance, c, m_instance->d_ptr->sessionExpires);
234 session = SessionPrivate::initializeSessionData(m_instance, c);
235 }
236 }
237
238 QVariantHash data = session.toHash();
239 for (const QString &key : keys) {
240 data.remove(key);
241 }
242
243 c->setStash(SESSION_VALUES, data);
244 c->setStash(SESSION_UPDATED, true);
245}
246
248{
249 return !SessionPrivate::loadSession(c).isNull();
250}
251
252QByteArray SessionPrivate::generateSessionId()
253{
254 return QUuid::createUuid().toRfc4122().toHex();
255}
256
257QByteArray SessionPrivate::loadSessionId(Context *c, const QByteArray &sessionName)
258{
259 QByteArray ret;
260 if (!c->stash(SESSION_TRIED_LOADING_ID).isNull()) {
261 return ret;
262 }
263 c->setStash(SESSION_TRIED_LOADING_ID, true);
264
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;
269 return ret;
270 }
271 ret = sid;
272 c->setStash(SESSION_ID, sid);
273 }
274
275 return ret;
276}
277
278QByteArray SessionPrivate::getSessionId(Context *c, const QByteArray &sessionName)
279{
280 QByteArray ret;
281 bool deleted = !c->stash(SESSION_DELETED_ID).isNull();
282
283 if (!deleted) {
284 const QVariant property = c->stash(SESSION_ID);
285 if (!property.isNull()) {
286 ret = property.toByteArray();
287 return ret;
288 }
289
290 const QByteArray cookie = c->request()->cookie(sessionName);
291 if (!cookie.isEmpty()) {
292 qCDebug(C_SESSION) << "Found sessionid" << cookie << "in cookie";
293 ret = cookie;
294 }
295 }
296
297 return ret;
298}
299
300QByteArray SessionPrivate::createSessionIdIfNeeded(Session *session, Context *c, qint64 expires)
301{
302 QByteArray ret;
303 const QVariant sid = c->stash(SESSION_ID);
304 if (!sid.isNull()) {
305 ret = sid.toByteArray();
306 } else {
307 ret = createSessionId(session, c, expires);
308 }
309 return ret;
310}
311
312QByteArray SessionPrivate::createSessionId(Session *session, Context *c, qint64 expires)
313{
314 Q_UNUSED(expires)
315 const auto sid = generateSessionId();
316
317 qCDebug(C_SESSION) << "Created session" << sid;
318
319 c->setStash(SESSION_ID, sid);
320 resetSessionExpires(session, c, sid);
321 setSessionId(session, c, sid);
322
323 return sid;
324}
325
326void SessionPrivate::_q_saveSession(Context *c)
327{
328 // fix cookie before we send headers
329 saveSessionExpires(c);
330
331 // Force extension of session_expires before finalizing headers, so a pos
332 // up to date. First call to session_expires will extend the expiry, methods
333 // just return the previously extended value.
335
336 // Persist data
337 if (Q_UNLIKELY(!m_instance)) {
338 qCCritical(C_SESSION) << "Session plugin not registered";
339 return;
340 }
341 saveSessionExpires(c);
342
343 if (!c->stash(SESSION_UPDATED).toBool()) {
344 return;
345 }
346 QVariantHash sessionData = c->stash(SESSION_VALUES).toHash();
347 sessionData.insert(QStringLiteral("__updated"), QDateTime::currentMSecsSinceEpoch() / 1000);
348
349 const auto sid = c->stash(SESSION_ID).toByteArray();
350 m_instance->d_ptr->store->storeSessionData(c, sid, QStringLiteral("session"), sessionData);
351}
352
353void SessionPrivate::deleteSession(Session *session, Context *c, const QString &reason)
354{
355 qCDebug(C_SESSION) << "Deleting session" << reason;
356
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"));
363
364 deleteSessionId(session, c, sid);
365 }
366
367 // Reset the values in Context object
368 c->setStash(SESSION_VALUES, QVariant());
369 c->setStash(SESSION_ID, QVariant());
370 c->setStash(SESSION_EXPIRES, QVariant());
371
372 c->setStash(SESSION_DELETE_REASON, reason);
373}
374
375void SessionPrivate::deleteSessionId(Session *session, Context *c, const QByteArray &sid)
376{
377 c->setStash(SESSION_DELETED_ID, true); // to prevent get_session_id from returning it
378
379 updateSessionCookie(c, makeSessionCookie(session, c, sid, QDateTime::currentDateTimeUtc()));
380}
381
382QVariant SessionPrivate::loadSession(Context *c)
383{
384 QVariant ret;
385 const QVariant property = c->stash(SESSION_VALUES);
386 if (!property.isNull()) {
387 ret = property.toHash();
388 return ret;
389 }
390
391 if (Q_UNLIKELY(!m_instance)) {
392 qCCritical(C_SESSION) << "Session plugin not registered";
393 return ret;
394 }
395
396 const auto sid = Session::id(c);
397 if (!loadSessionExpires(m_instance, c, sid).isNull()) {
398 if (SessionPrivate::validateSessionId(sid)) {
399
400 const QVariantHash sessionData =
401 m_instance->d_ptr->store->getSessionData(c, sid, QStringLiteral("session"))
402 .toHash();
403 c->setStash(SESSION_VALUES, sessionData);
404
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"));
413 return ret;
414 }
415
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()) {
420 qCWarning(C_SESSION)
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"));
425 return ret;
426 }
427 }
428
429 qCDebug(C_SESSION) << "Restored session" << sid;
430
431 ret = sessionData;
432 }
433 }
434
435 return ret;
436}
437
438bool SessionPrivate::validateSessionId(QByteArrayView id)
439{
440 auto it = id.begin();
441 auto end = id.end();
442 while (it != end) {
443 char c = *it;
444 if ((c >= 'a' && c <= 'f') || (c >= '0' && c <= '9')) {
445 ++it;
446 continue;
447 }
448 return false;
449 }
450
451 return id.size();
452}
453
454qint64 SessionPrivate::extendSessionExpires(Session *session, Context *c, qint64 expires)
455{
456 const qint64 threshold = qint64(session->d_ptr->expiryThreshold);
457
458 const auto sid = Session::id(c);
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;
463
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);
468
469 return updated;
470 } else {
471 return current;
472 }
473 } else {
474 return expires;
475 }
476}
477
478qint64 SessionPrivate::getStoredSessionExpires(Session *session,
479 Context *c,
480 const QByteArray &sessionid)
481{
482 const QVariant expires =
483 session->d_ptr->store->getSessionData(c, sessionid, QStringLiteral("expires"), 0);
484 return expires.toLongLong();
485}
486
487QVariant SessionPrivate::initializeSessionData(Session *session, Context *c)
488{
489 QVariantHash ret;
490 const qint64 now = QDateTime::currentMSecsSinceEpoch() / 1000;
491 ret.insert(QStringLiteral("__created"), now);
492 ret.insert(QStringLiteral("__updated"), now);
493
494 if (session->d_ptr->verifyAddress) {
495 ret.insert(QStringLiteral("__address"), c->request()->address().toString());
496 }
497
498 if (session->d_ptr->verifyUserAgent) {
499 ret.insert(QStringLiteral("__user_agent"), c->request()->userAgent());
500 }
501
502 return ret;
503}
504
505void SessionPrivate::saveSessionExpires(Context *c)
506{
507 const QVariant expires = c->stash(SESSION_EXPIRES);
508 if (!expires.isNull()) {
509 const auto sid = Session::id(c);
510 if (!sid.isEmpty()) {
511 if (Q_UNLIKELY(!m_instance)) {
512 qCCritical(C_SESSION) << "Session plugin not registered";
513 return;
514 }
515
516 const qint64 current = getStoredSessionExpires(m_instance, c, sid);
517 const qint64 extended = qint64(Session::expires(c));
518 if (extended > current) {
519 m_instance->d_ptr->store->storeSessionData(
520 c, sid, QStringLiteral("expires"), extended);
521 }
522 }
523 }
524}
525
526QVariant
527 SessionPrivate::loadSessionExpires(Session *session, Context *c, const QByteArray &sessionId)
528{
529 QVariant ret;
530 if (c->stash(SESSION_TRIED_LOADING_EXPIRES).toBool()) {
531 ret = c->stash(SESSION_EXPIRES);
532 return ret;
533 }
534 c->setStash(SESSION_TRIED_LOADING_EXPIRES, true);
535
536 if (!sessionId.isEmpty()) {
537 const qint64 expires = getStoredSessionExpires(session, c, sessionId);
538
539 if (expires >= QDateTime::currentMSecsSinceEpoch() / 1000) {
540 c->setStash(SESSION_EXPIRES, expires);
541 ret = expires;
542 } else {
543 deleteSession(session, c, QStringLiteral("session expired"));
544 ret = 0;
545 }
546 }
547 return ret;
548}
549
550qint64 SessionPrivate::initialSessionExpires(Session *session, Context *c)
551{
552 Q_UNUSED(c)
553 const qint64 expires = qint64(session->d_ptr->sessionExpires);
554 return QDateTime::currentMSecsSinceEpoch() / 1000 + expires;
555}
556
557qint64 SessionPrivate::calculateInitialSessionExpires(Session *session,
558 Context *c,
559 const QByteArray &sessionId)
560{
561 const qint64 stored = getStoredSessionExpires(session, c, sessionId);
562 const qint64 initial = initialSessionExpires(session, c);
563 return qMax(initial, stored);
564}
565
566qint64
567 SessionPrivate::resetSessionExpires(Session *session, Context *c, const QByteArray &sessionId)
568{
569 const qint64 exp = calculateInitialSessionExpires(session, c, sessionId);
570
571 c->setStash(SESSION_EXPIRES, exp);
572
573 // since we're setting _session_expires directly, make loadSessionExpires
574 // actually use that value.
575 c->setStash(SESSION_TRIED_LOADING_EXPIRES, true);
576 c->setStash(SESSION_EXTENDED_EXPIRES, exp);
577
578 return exp;
579}
580
581void SessionPrivate::updateSessionCookie(Context *c, const QNetworkCookie &updated)
582{
583 c->response()->setCookie(updated);
584}
585
586QNetworkCookie SessionPrivate::makeSessionCookie(Session *session,
587 Context *c,
588 const QByteArray &sid,
589 const QDateTime &expires)
590{
591 Q_UNUSED(c)
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);
598
599 return cookie;
600}
601
602void SessionPrivate::extendSessionId(Session *session,
603 Context *c,
604 const QByteArray &sid,
605 qint64 expires)
606{
607 updateSessionCookie(
608 c, makeSessionCookie(session, c, sid, QDateTime::fromMSecsSinceEpoch(expires * 1000)));
609}
610
611void SessionPrivate::setSessionId(Session *session, Context *c, const QByteArray &sid)
612{
613 updateSessionCookie(c,
614 makeSessionCookie(session,
615 c,
616 sid,
617 QDateTime::fromMSecsSinceEpoch(
618 initialSessionExpires(session, c) * 1000)));
619}
620
622 : QObject(parent)
623{
624}
625
626#include "moc_session.cpp"
The Cutelyst Application.
Definition: application.h:43
Engine * engine() const noexcept
void afterDispatch(Cutelyst::Context *c)
void postForked(Cutelyst::Application *app)
The Cutelyst Context.
Definition: context.h:38
void stash(const QVariantHash &unite)
Definition: context.cpp:553
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:211
Response * response() const noexcept
Definition: context.cpp:96
QVariantMap config(const QString &entity) const
user configuration for the application
Definition: engine.cpp:290
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:272
QHostAddress address() const noexcept
Definition: request.cpp:33
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:197
SessionStore(QObject *parent=nullptr)
Definition: session.cpp:621
static void deleteSession(Context *c, const QString &reason=QString())
Definition: session.cpp:144
static QString deleteReason(Context *c)
Definition: session.cpp:153
virtual bool setup(Application *app) final
Definition: session.cpp:45
Session(Application *parent)
Definition: session.cpp:34
static bool isValid(Context *c)
Definition: session.cpp:247
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:158
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:173
void setStorage(std::unique_ptr< SessionStore > store)
Definition: session.cpp:79
static void changeExpires(Context *c, quint64 expires)
Definition: session.cpp:131
static QByteArray id(Context *c)
Definition: session.cpp:93
SessionStore * storage() const
Definition: session.cpp:87
static void deleteValue(Context *c, const QString &key)
Definition: session.cpp:197
static quint64 expires(Context *c)
Definition: session.cpp:111
static void deleteValues(Context *c, const QStringList &keys)
Definition: session.cpp:221
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:8