Cutelyst  2.13.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 
203 void Session::deleteValue(Context *c, const QString &key)
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 
226 void Session::deleteValues(Context *c, const QStringList &keys)
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() && !validateSessionId(sid)) {
271  qCCritical(C_SESSION) << "Tried to set invalid session ID" << sid;
272  return ret;
273  }
274 
275  ret = sid;
276  c->setStash(SESSION_ID, sid);
277  return ret;
278 }
279 
280 QString SessionPrivate::getSessionId(Context *c, const QString &sessionName)
281 {
282  QString ret;
283  bool deleted = !c->stash(SESSION_DELETED_ID).isNull();
284 
285  if (!deleted) {
286  const QVariant property = c->stash(SESSION_ID);
287  if (!property.isNull()) {
288  ret = property.toString();
289  return ret;
290  }
291 
292  const QString cookie = c->request()->cookie(sessionName);
293  if (!cookie.isEmpty()) {
294  qCDebug(C_SESSION) << "Found sessionid" << cookie << "in cookie";
295  ret = cookie;
296  }
297  }
298 
299  return ret;
300 }
301 
302 QString SessionPrivate::createSessionIdIfNeeded(Session *session, Context *c, qint64 expires)
303 {
304  QString ret;
305  const QVariant sid = c->stash(SESSION_ID);
306  if (!sid.isNull()) {
307  ret = sid.toString();
308  } else {
309  ret = createSessionId(session, c, expires);
310  }
311  return ret;
312 }
313 
314 QString SessionPrivate::createSessionId(Session *session, Context *c, qint64 expires)
315 {
316  Q_UNUSED(expires)
317  const QString sid = generateSessionId();
318 
319  qCDebug(C_SESSION) << "Created session" << sid;
320 
321  c->setStash(SESSION_ID, sid);
322  resetSessionExpires(session, c, sid);
323  setSessionId(session, c, sid);
324 
325  return sid;
326 }
327 
328 void SessionPrivate::_q_saveSession(Context *c)
329 {
330  // fix cookie before we send headers
331  saveSessionExpires(c);
332 
333  // Force extension of session_expires before finalizing headers, so a pos
334  // up to date. First call to session_expires will extend the expiry, methods
335  // just return the previously extended value.
336  Session::expires(c);
337 
338  // Persist data
339  if (Q_UNLIKELY(!m_instance)) {
340  qCCritical(C_SESSION) << "Session plugin not registered";
341  return;
342  }
343  saveSessionExpires(c);
344 
345  if (!c->stash(SESSION_UPDATED).toBool()) {
346  return;
347  }
348  SessionStore *store = m_instance->d_ptr->store;
349  QVariantHash sessionData = c->stash(SESSION_VALUES).toHash();
350  sessionData.insert(QStringLiteral("__updated"), QDateTime::currentMSecsSinceEpoch() / 1000);
351 
352  const QString sid = c->stash(SESSION_ID).toString();
353  store->storeSessionData(c, sid, QStringLiteral("session"), sessionData);
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 qint64 SessionPrivate::extendSessionExpires(Session *session, Context *c, qint64 expires)
454 {
455  const qint64 threshold = qint64(session->d_ptr->expiryThreshold);
456 
457  const QString sid = Session::id(c);
458  if (!sid.isEmpty()) {
459  const qint64 current = getStoredSessionExpires(session, c, sid);
460  const qint64 cutoff = current - threshold;
461  const qint64 time = QDateTime::currentMSecsSinceEpoch() / 1000;
462 
463  if (!threshold || cutoff <= time || c->stash(SESSION_UPDATED).toBool()) {
464  qint64 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 qint64 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.toLongLong();
481 }
482 
483 QVariant SessionPrivate::initializeSessionData(Session *session, Context *c)
484 {
485  QVariantHash ret;
486  const qint64 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 qint64 current = getStoredSessionExpires(m_instance, c, sid);
513  const qint64 extended = qint64(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 qint64 expires = getStoredSessionExpires(session, c, sessionId);
532 
533  if (expires >= 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 qint64 SessionPrivate::initialSessionExpires(Session *session, Context *c)
545 {
546  Q_UNUSED(c)
547  const qint64 expires = qint64(session->d_ptr->sessionExpires);
548  return QDateTime::currentMSecsSinceEpoch() / 1000 + expires;
549 }
550 
551 qint64 SessionPrivate::calculateInitialSessionExpires(Session *session, Context *c, const QString &sessionId)
552 {
553  const qint64 stored = getStoredSessionExpires(session, c, sessionId);
554  const qint64 initial = initialSessionExpires(session, c);
555  return qMax(initial , stored);
556 }
557 
558 qint64 SessionPrivate::resetSessionExpires(Session *session, Context *c, const QString &sessionId)
559 {
560  const qint64 exp = calculateInitialSessionExpires(session, c, sessionId);
561 
562  c->setStash(SESSION_EXPIRES, exp);
563 
564  // since we're setting _session_expires directly, make loadSessionExpires
565  // actually use that value.
566  c->setStash(SESSION_TRIED_LOADING_EXPIRES, true);
567  c->setStash(SESSION_EXTENDED_EXPIRES, exp);
568 
569  return exp;
570 }
571 
572 void SessionPrivate::updateSessionCookie(Context *c, const QNetworkCookie &updated)
573 {
574  c->response()->setCookie(updated);
575 }
576 
577 QNetworkCookie SessionPrivate::makeSessionCookie(Session *session, Context *c, const QString &sid, const QDateTime &expires)
578 {
579  Q_UNUSED(c)
580  QNetworkCookie cookie(session->d_ptr->sessionName.toLatin1(), sid.toLatin1());
581  cookie.setPath(QStringLiteral("/"));
582  cookie.setExpirationDate(expires);
583  cookie.setHttpOnly(session->d_ptr->cookieHttpOnly);
584  cookie.setSecure(session->d_ptr->cookieSecure);
585 
586  return cookie;
587 }
588 
589 void SessionPrivate::extendSessionId(Session *session, Context *c, const QString &sid, qint64 expires)
590 {
591  updateSessionCookie(c, makeSessionCookie(session, c, sid, QDateTime::fromMSecsSinceEpoch(expires * 1000)));
592 }
593 
594 void SessionPrivate::setSessionId(Session *session, Context *c, const QString &sid)
595 {
596  updateSessionCookie(c, makeSessionCookie(session, c, sid,
597  QDateTime::fromMSecsSinceEpoch(initialSessionExpires(session, c) * 1000)));
598 }
599 
600 SessionStore::SessionStore(QObject *parent) : QObject(parent)
601 {
602 
603 }
604 
605 #include "moc_session.cpp"
Cutelyst::Application::postForked
void postForked(Cutelyst::Application *app)
Cutelyst::Plugin
Definition: plugin.h:30
Cutelyst::Session::storage
SessionStore * storage() const
Definition: session.cpp:94
Cutelyst::Session
Definition: session.h:105
Cutelyst::Application
The Cutelyst Application.
Definition: application.h:55
Cutelyst::Session::setValue
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:180
Cutelyst::Context
The Cutelyst Context.
Definition: context.h:50
Cutelyst::SessionStore::SessionStore
SessionStore(QObject *parent=nullptr)
Definition: session.cpp:600
Cutelyst::Context::setStash
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:225
Cutelyst::Context::response
Response * response() const
Definition: context.cpp:110
Cutelyst::Response::setCookie
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:228
Cutelyst::Session::expires
static quint64 expires(Context *c)
Definition: session.cpp:118
Cutelyst::Session::deleteValue
static void deleteValue(Context *c, const QString &key)
Definition: session.cpp:203
Cutelyst::SessionStoreFile
Definition: sessionstorefile.h:27
Cutelyst::Application::engine
Engine * engine() const
Definition: application.cpp:243
Cutelyst::Session::setup
virtual bool setup(Application *app) final
Definition: session.cpp:59
Cutelyst::SessionStore::storeSessionData
virtual bool storeSessionData(Context *c, const QString &sid, const QString &key, const QVariant &value)=0
Cutelyst::SessionStore
Definition: session.h:29
Cutelyst::Session::setStorage
void setStorage(SessionStore *store)
Definition: session.cpp:84
Cutelyst::Session::Session
Session(Application *parent)
Definition: session.cpp:48
Cutelyst::Session::deleteReason
static QString deleteReason(Context *c)
Definition: session.cpp:160
Cutelyst::Application::afterDispatch
void afterDispatch(Cutelyst::Context *c)
Cutelyst
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:7
Cutelyst::Session::deleteValues
static void deleteValues(Context *c, const QStringList &keys)
Definition: session.cpp:226
Cutelyst::Engine::config
QVariantMap config(const QString &entity) const
user configuration for the application
Definition: engine.cpp:320
Cutelyst::Session::value
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:165
Cutelyst::Request::address
QHostAddress address() const
Definition: request.cpp:46
Cutelyst::Request::cookie
QString cookie(const QString &name) const
Definition: request.cpp:285
Cutelyst::Session::isValid
static bool isValid(Context *c)
Definition: session.cpp:251
Cutelyst::Session::deleteSession
static void deleteSession(Context *c, const QString &reason=QString())
Definition: session.cpp:151
Cutelyst::Session::id
static QString id(Context *c)
Definition: session.cpp:100
Cutelyst::Session::changeExpires
static void changeExpires(Context *c, quint64 expires)
Definition: session.cpp:138
Cutelyst::Context::stash
void stash(const QVariantHash &unite)
Definition: context.h:558