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
Session(Cutelyst::Application * parent)48 Session::Session(Cutelyst::Application *parent) : Plugin(parent)
49 , d_ptr(new SessionPrivate(this))
50 {
51
52 }
53
~Session()54 Cutelyst::Session::~Session()
55 {
56 delete d_ptr;
57 }
58
setup(Application * app)59 bool Session::setup(Application *app)
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
setStorage(SessionStore * store)84 void Session::setStorage(SessionStore *store)
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
storage() const94 SessionStore *Session::storage() const
95 {
96 Q_D(const Session);
97 return d->store;
98 }
99
id(Cutelyst::Context * c)100 QString Session::id(Cutelyst::Context *c)
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
expires(Context * c)118 quint64 Session::expires(Context *c)
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
changeExpires(Context * c,quint64 expires)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
deleteSession(Context * c,const QString & reason)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
deleteReason(Context * c)160 QString Session::deleteReason(Context *c)
161 {
162 return c->stash(SESSION_DELETE_REASON).toString();
163 }
164
value(Cutelyst::Context * c,const QString & key,const QVariant & defaultValue)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
setValue(Cutelyst::Context * c,const QString & key,const QVariant & value)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
deleteValue(Context * c,const QString & key)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
deleteValues(Context * c,const QStringList & keys)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
isValid(Cutelyst::Context * c)251 bool Session::isValid(Cutelyst::Context *c)
252 {
253 return !SessionPrivate::loadSession(c).isNull();
254 }
255
generateSessionId()256 QString SessionPrivate::generateSessionId()
257 {
258 return QString::fromLatin1(QUuid::createUuid().toRfc4122().toHex());
259 }
260
loadSessionId(Context * c,const QString & sessionName)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
getSessionId(Context * c,const QString & sessionName)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
createSessionIdIfNeeded(Session * session,Context * c,qint64 expires)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
createSessionId(Session * session,Context * c,qint64 expires)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
_q_saveSession(Context * c)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
deleteSession(Session * session,Context * c,const QString & reason)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
deleteSessionId(Session * session,Context * c,const QString & sid)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
loadSession(Context * c)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
validateSessionId(const QString & id)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
extendSessionExpires(Session * session,Context * c,qint64 expires)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
getStoredSessionExpires(Session * session,Context * c,const QString & sessionid)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
initializeSessionData(Session * session,Context * c)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
saveSessionExpires(Context * c)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
loadSessionExpires(Session * session,Context * c,const QString & sessionId)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
initialSessionExpires(Session * session,Context * c)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
calculateInitialSessionExpires(Session * session,Context * c,const QString & sessionId)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
resetSessionExpires(Session * session,Context * c,const QString & sessionId)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
updateSessionCookie(Context * c,const QNetworkCookie & updated)574 void SessionPrivate::updateSessionCookie(Context *c, const QNetworkCookie &updated)
575 {
576 c->response()->setCookie(updated);
577 }
578
makeSessionCookie(Session * session,Context * c,const QString & sid,const QDateTime & expires)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
extendSessionId(Session * session,Context * c,const QString & sid,qint64 expires)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
setSessionId(Session * session,Context * c,const QString & sid)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
SessionStore(QObject * parent)602 SessionStore::SessionStore(QObject *parent) : QObject(parent)
603 {
604
605 }
606
607 #include "moc_session.cpp"
608