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