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