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 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")
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(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(QStringLiteral("__updated"), QDateTime::currentSecsSinceEpoch());
361
362 const auto sid = c->stash(SESSION_ID).toByteArray();
363 m_instance->d_ptr->store->storeSessionData(c, sid, QStringLiteral("session"), 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, QStringLiteral("session"));
374 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral("expires"));
375 session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral("flash"));
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, QStringLiteral("session"))
415 .toHash();
416 c->setStash(SESSION_VALUES, sessionData);
417
418 if (m_instance->d_ptr->verifyAddress) {
419 auto it = sessionData.constFind(u"__address"_s);
420 if (it != sessionData.constEnd() &&
421 it->toString() != c->request()->address().toString()) {
422 qCWarning(C_SESSION)
423 << "Deleting session" << sid << "due to address mismatch:" << *it
424 << "!=" << c->request()->address().toString();
425 deleteSession(m_instance, c, QStringLiteral("address mismatch"));
426 return ret;
427 }
428 }
429
430 if (m_instance->d_ptr->verifyUserAgent) {
431 auto it = sessionData.constFind(u"__user_agent"_s);
432 if (it != sessionData.constEnd() &&
433 it->toByteArray() != c->request()->userAgent()) {
434 qCWarning(C_SESSION)
435 << "Deleting session" << sid << "due to user agent mismatch:" << *it
436 << "!=" << c->request()->userAgent();
437 deleteSession(m_instance, c, QStringLiteral("user agent mismatch"));
438 return ret;
439 }
440 }
441
442 qCDebug(C_SESSION) << "Restored session" << sid << "keys" << sessionData.size();
443
444 ret = sessionData;
445 }
446 }
447
448 return ret;
449}
450
451bool SessionPrivate::validateSessionId(QByteArrayView id)
452{
453 for (auto c : id) {
454 if ((c >= 'a' && c <= 'f') || (c >= '0' && c <= '9')) {
455 continue;
456 }
457 return false;
458 }
459
460 return !id.empty();
461}
462
463qint64 SessionPrivate::extendSessionExpires(Session *session, Context *c, qint64 expires)
464{
465 const qint64 threshold = session->d_ptr->expiryThreshold;
466
467 const auto sid = Session::id(c);
468 if (!sid.isEmpty()) {
469 const qint64 current = getStoredSessionExpires(session, c, sid);
470 const qint64 cutoff = current - threshold;
471 const qint64 time = QDateTime::currentSecsSinceEpoch();
472
473 if (!threshold || cutoff <= time || c->stash(SESSION_UPDATED).toBool()) {
474 qint64 updated = calculateInitialSessionExpires(session, c, sid);
475 c->setStash(SESSION_EXTENDED_EXPIRES, updated);
476 extendSessionId(session, c, sid, updated);
477
478 return updated;
479 } else {
480 return current;
481 }
482 } else {
483 return expires;
484 }
485}
486
487qint64 SessionPrivate::getStoredSessionExpires(Session *session,
488 Context *c,
489 const QByteArray &sessionid)
490{
491 const QVariant expires =
492 session->d_ptr->store->getSessionData(c, sessionid, QStringLiteral("expires"), 0);
493 return expires.toLongLong();
494}
495
496QVariant SessionPrivate::initializeSessionData(Session *session, Context *c)
497{
498 QVariantHash ret;
499 const qint64 now = QDateTime::currentSecsSinceEpoch();
500 ret.insert(QStringLiteral("__created"), now);
501 ret.insert(QStringLiteral("__updated"), now);
502
503 if (session->d_ptr->verifyAddress) {
504 ret.insert(QStringLiteral("__address"), c->request()->address().toString());
505 }
506
507 if (session->d_ptr->verifyUserAgent) {
508 ret.insert(QStringLiteral("__user_agent"), c->request()->userAgent());
509 }
510
511 return ret;
512}
513
514void SessionPrivate::saveSessionExpires(Context *c)
515{
516 const QVariant expires = c->stash(SESSION_EXPIRES);
517 if (!expires.isNull()) {
518 const auto sid = Session::id(c);
519 if (!sid.isEmpty()) {
520 if (Q_UNLIKELY(!m_instance)) {
521 qCCritical(C_SESSION) << "Session plugin not registered";
522 return;
523 }
524
525 const qint64 current = getStoredSessionExpires(m_instance, c, sid);
526 const qint64 extended = Session::expires(c);
527 if (extended > current) {
528 m_instance->d_ptr->store->storeSessionData(
529 c, sid, QStringLiteral("expires"), extended);
530 }
531 }
532 }
533}
534
536 SessionPrivate::loadSessionExpires(Session *session, Context *c, const QByteArray &sessionId)
537{
538 QVariant ret;
539 if (c->stash(SESSION_TRIED_LOADING_EXPIRES).toBool()) {
540 ret = c->stash(SESSION_EXPIRES);
541 return ret;
542 }
543 c->setStash(SESSION_TRIED_LOADING_EXPIRES, true);
544
545 if (!sessionId.isEmpty()) {
546 const qint64 expires = getStoredSessionExpires(session, c, sessionId);
547
548 if (expires >= QDateTime::currentSecsSinceEpoch()) {
549 c->setStash(SESSION_EXPIRES, expires);
550 ret = expires;
551 } else {
552 deleteSession(session, c, QStringLiteral("session expired"));
553 ret = 0;
554 }
555 }
556 return ret;
557}
558
559qint64 SessionPrivate::initialSessionExpires(Session *session, Context *c)
560{
561 Q_UNUSED(c)
562 const qint64 expires = session->d_ptr->sessionExpires;
563 return QDateTime::currentSecsSinceEpoch() + expires;
564}
565
566qint64 SessionPrivate::calculateInitialSessionExpires(Session *session,
567 Context *c,
568 const QByteArray &sessionId)
569{
570 const qint64 stored = getStoredSessionExpires(session, c, sessionId);
571 const qint64 initial = initialSessionExpires(session, c);
572 return qMax(initial, stored);
573}
574
575qint64
576 SessionPrivate::resetSessionExpires(Session *session, Context *c, const QByteArray &sessionId)
577{
578 const qint64 exp = calculateInitialSessionExpires(session, c, sessionId);
579
580 c->setStash(SESSION_EXPIRES, exp);
581
582 // since we're setting _session_expires directly, make loadSessionExpires
583 // actually use that value.
584 c->setStash(SESSION_TRIED_LOADING_EXPIRES, true);
585 c->setStash(SESSION_EXTENDED_EXPIRES, exp);
586
587 return exp;
588}
589
590void SessionPrivate::updateSessionCookie(Context *c, const QNetworkCookie &updated)
591{
592 c->response()->setCookie(updated);
593}
594
595QNetworkCookie SessionPrivate::makeSessionCookie(Session *session,
596 Context *c,
597 const QByteArray &sid,
598 const QDateTime &expires)
599{
600 Q_UNUSED(c)
601 QNetworkCookie cookie(session->d_ptr->sessionName, sid);
602 cookie.setPath(u"/"_s);
603 cookie.setExpirationDate(expires);
604 cookie.setHttpOnly(session->d_ptr->cookieHttpOnly);
605 cookie.setSecure(session->d_ptr->cookieSecure);
606 cookie.setSameSitePolicy(session->d_ptr->cookieSameSite);
607
608 return cookie;
609}
610
611void SessionPrivate::extendSessionId(Session *session,
612 Context *c,
613 const QByteArray &sid,
614 qint64 expires)
615{
616 updateSessionCookie(c,
617 makeSessionCookie(session, c, sid, QDateTime::fromSecsSinceEpoch(expires)));
618}
619
620void SessionPrivate::setSessionId(Session *session, Context *c, const QByteArray &sid)
621{
622 updateSessionCookie(
623 c,
624 makeSessionCookie(
625 session, c, sid, QDateTime::fromSecsSinceEpoch(initialSessionExpires(session, c))));
626}
627
628QVariant SessionPrivate::config(const QString &key, const QVariant &defaultValue) const
629{
630 return loadedConfig.value(key, defaultConfig.value(key, defaultValue));
631}
632
634 : QObject(parent)
635{
636}
637
638#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:565
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:278
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:633
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