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