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