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
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
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
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
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
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.
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
QObject(QObject *parent)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
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