cutelyst 5.0.1
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
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
157void Session::deleteSession(Context *c, const QString &reason)
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
210void Session::deleteValue(Context *c, const QString &key)
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
234void Session::deleteValues(Context *c, const QStringList &keys)
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
527QVariant
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
Plugin(Application *parent)
Definition plugin.cpp:12
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.