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