MythTV master
mythsession.cpp
Go to the documentation of this file.
1
2#include "mythsession.h"
3
4#include "mythdbcon.h"
5#include "mythlogging.h"
6#include "mythcorecontext.h"
7#include "mythdate.h"
8
9#include <QCryptographicHash>
10#include <QUuid>
11
13
18{
19 if (m_userId == 0)
20 {
21 LOG(VB_GENERAL, LOG_WARNING, QString("IsValid: Invalid session "
22 "m_userId %1").arg(m_userId));
23 return false;
24 }
25 if (m_sessionToken.isEmpty() || m_sessionToken.length() != 40)
26 {
27 LOG(VB_GENERAL, LOG_WARNING, QString("IsValid: Invalid session "
28 "m_sessionToken %1").arg(m_sessionToken));
29 return false;
30 }
31
33 {
34 LOG(VB_GENERAL, LOG_WARNING, QString("IsValid: Invalid session "
35 "m_sessionCreated %1").arg(m_sessionCreated.toString()));
36 return false;
37 }
38
40 {
41 LOG(VB_GENERAL, LOG_WARNING, QString("IsValid: Expired session "
42 "m_sessionExpires %1").arg(m_sessionExpires.toString()));
43 return false;
44 }
45
46 return true;
47}
48
52bool MythUserSession::CheckPermission(const QString &/*context*/,
53 uint /*permission*/)
54{
55 Update();
56
57 // TODO: Implement perms checking here
58
59 return false;
60}
61
66{
67 if (m_userId == 0)
68 return false;
69
70 if (m_sessionToken.isEmpty())
71 {
72 QByteArray randBytes = QUuid::createUuid().toByteArray();
73 m_sessionToken = QCryptographicHash::hash(randBytes,
74 QCryptographicHash::Sha1).toHex();
75 }
76
77 if (m_sessionCreated.isNull())
79 if (m_sessionLastActive.isNull())
81 if (m_sessionExpires.isNull())
83
85 query.prepare("REPLACE INTO user_sessions SET "
86 "sessionToken = :SESSION_TOKEN, "
87 "userid = :USERID, "
88 "client = :CLIENT, "
89 "created = :CREATED, "
90 "lastactive = :LASTACTIVE, "
91 "expires = :EXPIRES");
92 query.bindValue(":SESSION_TOKEN", m_sessionToken);
93 query.bindValue(":USERID", m_userId);
94 query.bindValue(":CLIENT", m_sessionClient);
95 query.bindValue(":CREATED", m_sessionCreated);
96 query.bindValue(":LASTACTIVE", m_sessionLastActive);
97 // For now, fixed duration of 1 day, this should be different for the
98 // WebFrontend (external client) vs the Frontend (local client). It should
99 // perhaps be configurable. The expiry date is extended every time the
100 // the session is validated, for the WebFrontend this is every http
101 // connection for other clients this has yet to be defined
102 query.bindValue(":EXPIRES", m_sessionExpires);
103
104 return query.exec();
105}
106
111{
112 // Only update once per 5 minutes (300 seconds) at the most
113 QDateTime current = MythDate::current();
114 if (!m_sessionLastActive.isValid() || (m_sessionLastActive.addSecs(300) < current))
115 {
117 m_sessionExpires = current.addDays(1);
118 return Save();
119 }
120 return true;
121}
122
127{
128 LoadSessions();
129}
130
135{
136 m_sessionList.clear();
137
139 query.prepare("SELECT s.sessiontoken, s.created, s.lastactive, s.expires, "
140 " s.client, u.userid, u.username "
141 "FROM user_sessions s, users u "
142 "WHERE s.userid = u.userid ");
143
144 if (!query.exec())
145 MythDB::DBError("Error loading user sessions from database", query);
146
147 while (query.next())
148 {
149 MythUserSession session;
150
151 session.m_sessionToken = query.value(0).toString();
152 session.m_sessionCreated = MythDate::as_utc(query.value(1).toDateTime());
153 session.m_sessionLastActive = MythDate::as_utc(query.value(2).toDateTime());
154 session.m_sessionExpires = MythDate::as_utc(query.value(3).toDateTime());
155 session.m_sessionClient = query.value(4).toString();
156 session.m_userId = query.value(5).toUInt();
157 session.m_name = query.value(6).toString();
158
159 m_sessionList.insert(session.m_sessionToken, session);
160 }
161}
162
166bool MythSessionManager::IsValidUser(const QString& username)
167{
168 if (username.isEmpty())
169 return false;
170
172 query.prepare("SELECT userid FROM users WHERE username = :USERNAME");
173 query.bindValue(":USERNAME", username);
174
175 if (!query.exec())
176 MythDB::DBError("Error finding user", query);
177
178 return query.next();
179}
180
185{
186 if (IsValidSession(sessionToken))
187 return m_sessionList[sessionToken];
188
189 return {};
190}
191
196 const QString &client)
197{
198 QMap<QString, MythUserSession>::iterator it;
199 for (it = m_sessionList.begin(); it != m_sessionList.end(); ++it)
200 {
201 if (((*it).m_name == username) &&
202 ((*it).m_sessionClient == client))
203 {
204 if ((*it).IsValid())
205 {
206 (*it).Update();
207 return *it;
208 }
209
210 DestroyUserSession((*it).m_sessionToken);
211 break;
212 }
213 }
214
215 return {};
216}
217
221QString MythSessionManager::GetPasswordDigest(const QString& username)
222{
224 query.prepare("SELECT password_digest FROM users WHERE username = :USERNAME");
225 query.bindValue(":USERNAME", username);
226
227 if (!query.exec())
228 MythDB::DBError("Error finding user", query);
229
230 if (query.next())
231 return query.value(0).toString();
232
233 return {};
234}
235
239bool MythSessionManager::IsValidSession(const QString& sessionToken)
240{
241 if (m_sessionList.contains(sessionToken))
242 {
243 MythUserSession session = m_sessionList[sessionToken];
244 if (session.IsValid())
245 {
246 // Accessing a session automatically extends it
247 UpdateSession(sessionToken);
248 return true;
249 }
250
251 DestroyUserSession(sessionToken);
252 }
253
254 return false;
255}
256
260void MythSessionManager::UpdateSession(const QString& sessionToken)
261{
262 if (m_sessionList.contains(sessionToken))
263 {
264 MythUserSession session = m_sessionList[sessionToken];
265 session.Update(); // Update the database
266 m_sessionList[sessionToken] = session; // Update the cache
267 }
268}
269
274 const QByteArray &digest,
275 const QString &client)
276{
277 if (username.isEmpty() || digest.isEmpty() || digest.length() < 32 ||
278 digest.length() > 32)
279 return {};
280
282 query.prepare("SELECT userid, username FROM users WHERE "
283 "username = :USERNAME AND password_digest = :PWDIGEST");
284
285 query.bindValue(":USERNAME", username);
286 query.bindValue(":PWDIGEST", QString(digest));
287
288 if (!query.exec())
289 return {};
290
291 if (query.size() > 1)
292 {
293 LOG(VB_GENERAL, LOG_CRIT, "LoginUser: Warning, multiple matching user records found.");
294 return {};
295 }
296
297 if (query.next())
298 {
299 // Having verified the password, check if there is an existing session
300 // open that we should re-use. This is necessary for unencrypted HTTP
301 // Auth where the session token isn't stored on the client and we must
302 // re-auth on every request
303 MythUserSession session = GetSession(username, client);
304
305 if (session.IsValid())
306 return session;
307
308 // No pre-existing session, so create a new one
309 uint userId = query.value(0).toUInt();
310 QString userName = query.value(1).toString();
311
312 return CreateUserSession(userId, userName, client);
313 }
314
315 LOG(VB_GENERAL, LOG_WARNING, QString("LoginUser: Failed login attempt for "
316 "user %1").arg(username));
317
318 return {};
319}
320
325 const QString &password,
326 const QString &client)
327{
328 QByteArray digest = CreateDigest(username, password);
329
330 return LoginUser(username, digest, client);
331}
332
337 const QString &userName,
338 const QString &client)
339{
340 MythUserSession session;
341
342 session.m_userId = userId;
343 session.m_name = userName;
344
345 QString clientIdentifier = client;
346 if (clientIdentifier.isEmpty())
347 {
348 QString type = "Master";
350 type = "Slave";
351
352 clientIdentifier =
353 QString("%1_%2").arg(type, gCoreContext->GetHostName());
354 }
355
356 session.m_sessionClient = clientIdentifier;
357
360 session.m_sessionExpires = MythDate::current().addDays(1);
361
362 if (session.Save()) // Sets the session token
363 {
364 m_sessionList.insert(session.m_sessionToken, session);
365 }
366
367 return session;
368}
369
373void MythSessionManager::DestroyUserSession(const QString &sessionToken)
374{
375 if (sessionToken.isEmpty())
376 return;
377
379 query.prepare("DELETE FROM user_sessions WHERE "
380 "sessionToken = :SESSION_TOKEN");
381 query.bindValue(":SESSION_TOKEN", sessionToken);
382
383 if (!query.exec())
384 {
385 MythDB::DBError("Error deleting user session from database", query);
386 }
387
388 if (m_sessionList.contains(sessionToken))
389 m_sessionList.remove(sessionToken);
390}
391
393{
394 if (username.isEmpty())
395 return;
396
397 QMap<QString, MythUserSession>::iterator it;
398 while (it != m_sessionList.end())
399 {
400 for (it = m_sessionList.begin(); it != m_sessionList.end(); ++it)
401 {
402 if (((*it).m_name == username))
403 {
404 DestroyUserSession((*it).m_sessionToken);
405 // Restart since list is now changed
406 // Not the most efficient way to do it, but we do not expect
407 // more than one or two sessions anyway.
408 break;
409 }
410 }
411 }
412}
413
417bool MythSessionManager::AddDigestUser(const QString& username,
418 const QString& password)
419{
420 if (IsValidUser(username))
421 {
422 LOG(VB_GENERAL, LOG_ERR, QString("Tried to add an existing user: %1.")
423 .arg(username));
424 return false;
425 }
426
428 insert.prepare("INSERT INTO users SET "
429 "username = :USER_NAME, "
430 "password_digest = :PASSWORD_DIGEST");
431 insert.bindValue(":USER_NAME", username);
432 insert.bindValue(":PASSWORD_DIGEST", CreateDigest(username, password));
433
434 bool bResult = insert.exec();
435 if (!bResult)
436 MythDB::DBError("Error adding digest user to database", insert);
437
438 return bResult;
439}
440
444bool MythSessionManager::RemoveDigestUser(const QString& username)
445{
446 if (!IsValidUser(username))
447 {
448 LOG(VB_GENERAL, LOG_ERR, QString("Tried to remove a non-existing "
449 "user: %1.").arg(username));
450 return false;
451 }
452
453 if (username == "admin")
454 {
455 LOG(VB_GENERAL, LOG_ERR, QString("Tried to remove user: %1 (not "
456 "permitted.)").arg("admin"));
457 return false;
458 }
459
460 MSqlQuery deleteQuery(MSqlQuery::InitCon());
461 deleteQuery.prepare("DELETE FROM users WHERE " "username = :USER_NAME ");
462 deleteQuery.bindValue(":USER_NAME", username);
463
464 bool bResult = deleteQuery.exec();
465 if (bResult)
466 DestroyUserAllSessions(username);
467 else
468 MythDB::DBError("Error removing digest user from database",
469 deleteQuery);
470
471 return bResult;
472}
473
478 const QString& oldPassword,
479 const QString& newPassword)
480{
481 if (newPassword.isEmpty())
482 {
483 LOG(VB_GENERAL, LOG_ERR, QString("New password is missing."));
484 return false;
485 }
486
487 if (!IsValidUser(username))
488 {
489 LOG(VB_GENERAL, LOG_ERR, QString("Attempted to update non-existing"
490 " user: %1.").arg(username));
491 return false;
492 }
493
494 // Allow for empty old password so that admin can change a forgotten password
495 if (!oldPassword.isEmpty())
496 {
497 QByteArray oldPasswordDigest = CreateDigest(username, oldPassword);
498
499 if (oldPasswordDigest != GetPasswordDigest(username))
500 {
501 LOG(VB_GENERAL, LOG_ERR, QString("Incorrect old password for "
502 "user: %1.").arg(username));
503 return false;
504 }
505 }
506
508 update.prepare("UPDATE users SET "
509 "password_digest = :NEW_PASSWORD_DIGEST WHERE "
510 "username = :USER_NAME");
511 update.bindValue(":NEW_PASSWORD_DIGEST", CreateDigest(username,
512 newPassword));
513 update.bindValue(":USER_NAME", username);
514
515 bool bResult = update.exec();
516 if (bResult)
517 DestroyUserAllSessions(username);
518 else
519 MythDB::DBError("Error updating digest user in database", update);
520
521
522 return bResult;
523}
524
528QByteArray MythSessionManager::CreateDigest(const QString &username,
529 const QString &password)
530{
531 // The realm is a constant, it's not private because it's included in the
532 // plain text sent with an HTTP WWW-Authenticate header.
533 QString plainText = QString("%1:MythTV:%2").arg(username, password);
534 QByteArray digest = QCryptographicHash::hash(plainText.toLatin1(),
535 QCryptographicHash::Md5).toHex();
536 return digest;
537}
538
543 const QString& username,
544 const QString& password,
545 const QString& newPassword)
546{
547 bool returnCode = false;
548
549 if (action == DIGEST_USER_ADD)
550 returnCode = AddDigestUser(username, password); //, adminPassword);
551 else if (action == DIGEST_USER_REMOVE)
552 returnCode = RemoveDigestUser(username);
553 else if (action == DIGEST_USER_CHANGE_PW)
554 returnCode = ChangeDigestUserPassword(username, password, newPassword);
555 else
556 LOG(VB_GENERAL, LOG_ERR, QString("Unknown action."));
557
558 return returnCode;
559}
560
562{
563 mutex.lock();
564}
566{
567 mutex.unlock();
568}
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:837
QVariant value(int i) const
Definition: mythdbcon.h:204
int size(void) const
Definition: mythdbcon.h:214
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:618
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:888
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:812
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:550
QString GetHostName(void)
bool IsMasterBackend(void)
is this the actual MBE process
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:226
static bool IsValidUser(const QString &username)
Check if the given user exists but not whether there is a valid session open for them!
static QMutex mutex
Definition: mythsession.h:230
void UpdateSession(const QString &sessionToken)
Update the session timestamps.
bool IsValidSession(const QString &sessionToken)
Check if the session token is valid.
void DestroyUserAllSessions(const QString &username)
bool RemoveDigestUser(const QString &username)
static QString GetPasswordDigest(const QString &username)
Load the password digest for comparison in the HTTP Auth code.
bool ChangeDigestUserPassword(const QString &username, const QString &oldPassword, const QString &newPassword)
MythUserSession GetSession(const QString &sessionToken)
Load the session details and return.
static void UnlockSessions()
void LoadSessions(void)
Load the values from the sessions table on startup.
void DestroyUserSession(const QString &sessionToken)
Removes user session from the database and cache.
QMap< QString, MythUserSession > m_sessionList
Definition: mythsession.h:228
static QByteArray CreateDigest(const QString &username, const QString &password)
Generate a digest string.
bool ManageDigestUser(DigestUserActions action, const QString &username, const QString &password, const QString &newPassword)
Manage digest user entries.
MythUserSession CreateUserSession(uint userId, const QString &username, const QString &client)
Add new user session to the database and cache.
static void LockSessions()
static bool AddDigestUser(const QString &username, const QString &password)
MythUserSession LoginUser(const QString &username, const QByteArray &digest, const QString &client="")
Login user by digest.
QDateTime m_sessionCreated
Definition: mythsession.h:81
bool IsValid(void) const
Check if this session object appears properly constructed, it DOES NOT validate whether it is a valid...
Definition: mythsession.cpp:17
bool Save(void)
Save the session to the database.
Definition: mythsession.cpp:65
QDateTime m_sessionExpires
Definition: mythsession.h:83
bool CheckPermission(const QString &context, uint permission)
Check if the user has the given permission in a context.
Definition: mythsession.cpp:52
QString m_sessionToken
Definition: mythsession.h:80
QString m_name
Definition: mythsession.h:78
bool Update(void)
Update session expiry and access times.
QString m_sessionClient
Definition: mythsession.h:84
QDateTime m_sessionLastActive
Definition: mythsession.h:82
unsigned int uint
Definition: freesurround.h:24
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
DigestUserActions
Definition: mythsession.h:13
@ DIGEST_USER_ADD
Definition: mythsession.h:14
@ DIGEST_USER_CHANGE_PW
Definition: mythsession.h:16
@ DIGEST_USER_REMOVE
Definition: mythsession.h:15
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:28
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15