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 
15 bool MythUserSession::IsValid(void) const
16 {
17  if (m_userId == 0)
18  return false;
19 
20  if (m_sessionToken.isEmpty() || m_sessionToken.length() != 40)
21  return false;
22 
24  return false;
25 
27  return false;
28 
29  // NOTE: Check client string as well?
30 
31  return true;
32 }
33 
37 bool MythUserSession::CheckPermission(const QString &/*context*/,
38  uint /*permission*/)
39 {
41  {
42  // TODO: Connect to master and do checking there
43  return false;
44  }
45 
46  Update();
47 
48  // TODO: Implement perms checking here
49 
50  return false;
51 }
52 
57 {
58  if (m_userId == 0)
59  return false;
60 
61  if (m_sessionToken.isEmpty())
62  {
63  QByteArray randBytes = QUuid::createUuid().toByteArray();
64  m_sessionToken = QCryptographicHash::hash(randBytes,
65  QCryptographicHash::Sha1).toHex();
66  }
67 
68  if (m_sessionCreated.isNull())
70  if (m_sessionLastActive.isNull())
72  if (m_sessionExpires.isNull())
73  m_sessionExpires = MythDate::current().addDays(1);
74 
76  query.prepare("REPLACE INTO user_sessions SET "
77  "sessionToken = :SESSION_TOKEN, "
78  "userid = :USERID, "
79  "client = :CLIENT, "
80  "created = :CREATED, "
81  "lastactive = :LASTACTIVE, "
82  "expires = :EXPIRES");
83  query.bindValue(":SESSION_TOKEN", m_sessionToken);
84  query.bindValue(":USERID", m_userId);
85  query.bindValue(":CLIENT", m_sessionClient);
86  query.bindValue(":CREATED", m_sessionCreated);
87  query.bindValue(":LASTACTIVE", m_sessionLastActive);
88  // For now, fixed duration of 1 day, this should be different for the
89  // WebFrontend (external client) vs the Frontend (local client). It should
90  // perhaps be configurable. The expiry date is extended every time the
91  // the session is validated, for the WebFrontend this is every http
92  // connection for other clients this has yet to be defined
93  query.bindValue(":EXPIRES", m_sessionExpires);
94 
95  return query.exec();
96 }
97 
102 {
104  m_sessionExpires = MythDate::current().addDays(1);
105  return Save();
106 }
107 
112 {
114  LoadSessions();
115 }
116 
121 {
122  m_sessionList.clear();
123 
124  MSqlQuery query(MSqlQuery::InitCon());
125  query.prepare("SELECT s.sessiontoken, s.created, s.lastactive, s.expires, "
126  " s.client, u.userid, u.username "
127  "FROM user_sessions s, users u");
128 
129  if (!query.exec())
130  MythDB::DBError("Error loading user sessions from database", query);
131 
132  while (query.next())
133  {
134  MythUserSession session;
135 
136  session.m_sessionToken = query.value(0).toString();
137  session.m_sessionCreated = query.value(1).toDateTime();
138  session.m_sessionLastActive = query.value(2).toDateTime();
139  session.m_sessionExpires = query.value(3).toDateTime();
140  session.m_sessionClient = query.value(4).toString();
141  session.m_userId = query.value(5).toUInt();
142  session.m_name = query.value(6).toString();
143 
144  m_sessionList.insert(session.m_sessionToken, session);
145  }
146 }
147 
151 bool MythSessionManager::IsValidUser(const QString& username)
152 {
153  if (username.isEmpty())
154  return false;
155 
157  {
158  // TODO: Connect to master and do checking there
159  return false;
160  }
161 
162  MSqlQuery query(MSqlQuery::InitCon());
163  query.prepare("SELECT userid FROM users WHERE username = :USERNAME");
164  query.bindValue(":USERNAME", username);
165 
166  if (!query.exec())
167  MythDB::DBError("Error finding user", query);
168 
169  return query.next();
170 }
171 
176 {
178  {
179  // TODO: Connect to master and do checking there
180  return {};
181  }
182 
183  if (IsValidSession(sessionToken))
184  return m_sessionList[sessionToken];
185 
186  return {};
187 }
188 
193  const QString &client)
194 {
196  {
197  // TODO: Connect to master and do checking there
198  return {};
199  }
200 
201  QMap<QString, MythUserSession>::iterator it;
202  for (it = m_sessionList.begin(); it != m_sessionList.end(); ++it)
203  {
204  if (((*it).m_name == username) &&
205  ((*it).m_sessionClient == client))
206  {
207  if ((*it).IsValid())
208  {
209  (*it).Update();
210  return *it;
211  }
212 
213  DestroyUserSession((*it).m_sessionToken);
214  break;
215  }
216  }
217 
218  return {};
219 }
220 
224 QString MythSessionManager::GetPasswordDigest(const QString& username)
225 {
226  MSqlQuery query(MSqlQuery::InitCon());
227  query.prepare("SELECT password_digest FROM users WHERE username = :USERNAME");
228  query.bindValue(":USERNAME", username);
229 
230  if (!query.exec())
231  MythDB::DBError("Error finding user", query);
232 
233  if (query.next())
234  return query.value(0).toString();
235 
236  return {};
237 }
238 
242 bool MythSessionManager::IsValidSession(const QString& sessionToken)
243 {
245  {
246  // TODO: Connect to master and do checking there
247  return false;
248  }
249 
250  if (m_sessionList.contains(sessionToken))
251  {
252  MythUserSession session = m_sessionList[sessionToken];
253  if (session.IsValid())
254  {
255  // Accessing a session automatically extends it
256  UpdateSession(sessionToken);
257  return true;
258  }
259 
260  DestroyUserSession(sessionToken);
261  }
262 
263  return false;
264 }
265 
269 void MythSessionManager::UpdateSession(const QString& sessionToken)
270 {
271  if (m_sessionList.contains(sessionToken))
272  {
273  MythUserSession session = m_sessionList[sessionToken];
274  session.Update(); // Update the database
275  m_sessionList[sessionToken] = session; // Update the cache
276  }
277 }
278 
283  const QByteArray &digest,
284  const QString &client)
285 {
286  if (username.isEmpty() || digest.isEmpty() || digest.length() < 32 ||
287  digest.length() > 32)
288  return {};
289 
291  {
292  // TODO: Connect to master and do checking there
293  return {};
294  }
295 
296  MSqlQuery query(MSqlQuery::InitCon());
297  query.prepare("SELECT userid, username FROM users WHERE "
298  "username = :USERNAME AND password_digest = :PWDIGEST");
299 
300  query.bindValue(":USERNAME", username);
301  query.bindValue(":PWDIGEST", QString(digest));
302 
303  if (!query.exec())
304  return {};
305 
306  if (query.size() > 1)
307  {
308  LOG(VB_GENERAL, LOG_CRIT, "LoginUser: Warning, multiple matching user records found.");
309  return {};
310  }
311 
312  if (query.next())
313  {
314  // Having verified the password, check if there is an existing session
315  // open that we should re-use. This is necessary for unencrypted HTTP
316  // Auth where the session token isn't stored on the client and we must
317  // re-auth on every request
318  MythUserSession session = GetSession(username, client);
319 
320  if (session.IsValid())
321  return session;
322 
323  // No pre-existing session, so create a new one
324  uint userId = query.value(0).toUInt();
325  QString userName = query.value(1).toString();
326 
327  return CreateUserSession(userId, userName, client);
328  }
329 
330  LOG(VB_GENERAL, LOG_WARNING, QString("LoginUser: Failed login attempt for "
331  "user %1").arg(username));
332 
333  return {};
334 }
335 
340  const QString &password,
341  const QString &client)
342 {
343  QByteArray digest = CreateDigest(username, password);
344 
345  return LoginUser(username, digest, client);
346 }
347 
352  const QString &userName,
353  const QString &client)
354 {
355  MythUserSession session;
356 
357  session.m_userId = userId;
358  session.m_name = userName;
359 
360  QString clientIdentifier = client;
361  if (clientIdentifier.isEmpty())
362  {
363  QString type = "Master";
365  type = "Slave";
366 
367  clientIdentifier =
368  QString("%1_%2").arg(type, gCoreContext->GetHostName());
369  }
370 
371  session.m_sessionClient = clientIdentifier;
372 
375  session.m_sessionExpires = MythDate::current().addDays(1);
376 
377  if (session.Save()) // Sets the session token
378  {
379  m_sessionList.insert(session.m_sessionToken, session);
380  }
381 
382  return session;
383 }
384 
388 void MythSessionManager::DestroyUserSession(const QString &sessionToken)
389 {
390  if (sessionToken.isEmpty())
391  return;
392 
393  MSqlQuery query(MSqlQuery::InitCon());
394  query.prepare("DELETE FROM user_sessions WHERE "
395  "sessionToken = :SESSION_TOKEN");
396  query.bindValue(":SESSION_TOKEN", sessionToken);
397 
398  if (!query.exec())
399  {
400  MythDB::DBError("Error deleting user session from database", query);
401  }
402 
403  if (m_sessionList.contains(sessionToken))
404  m_sessionList.remove(sessionToken);
405 }
406 
410 bool MythSessionManager::AddDigestUser(const QString& username,
411  const QString& password,
412  const QString& adminPassword)
413 {
414  if (adminPassword.isEmpty())
415  {
416  LOG(VB_GENERAL, LOG_ERR, QString("Admin password is missing."));
417  return false;
418  }
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 
427  if (CreateDigest("admin", adminPassword) != GetPasswordDigest("admin"))
428  {
429  LOG(VB_GENERAL, LOG_ERR, QString("Incorrect password for user: %1.")
430  .arg("admin"));
431  return false;
432  }
433 
434  MSqlQuery insert(MSqlQuery::InitCon());
435  insert.prepare("INSERT INTO users SET "
436  "username = :USER_NAME, "
437  "password_digest = :PASSWORD_DIGEST");
438  insert.bindValue(":USER_NAME", username);
439  insert.bindValue(":PASSWORD_DIGEST", CreateDigest(username, password));
440 
441  bool bResult = insert.exec();
442  if (!bResult)
443  MythDB::DBError("Error adding digest user to database", insert);
444 
445  return bResult;
446 }
447 
451 bool MythSessionManager::RemoveDigestUser(const QString& username,
452  const QString& password)
453 {
454  if (!IsValidUser(username))
455  {
456  LOG(VB_GENERAL, LOG_ERR, QString("Tried to remove a non-existing "
457  "user: %1.").arg(username));
458  return false;
459  }
460 
461  if (username == "admin")
462  {
463  LOG(VB_GENERAL, LOG_ERR, QString("Tried to remove user: %1 (not "
464  "permitted.)").arg("admin"));
465  return false;
466  }
467 
468  if (CreateDigest(username, password) != GetPasswordDigest(username))
469  {
470  LOG(VB_GENERAL, LOG_ERR, QString("Incorrect password for user: %1.")
471  .arg(username));
472  return false;
473  }
474 
475  MSqlQuery deleteQuery(MSqlQuery::InitCon());
476  deleteQuery.prepare("DELETE FROM users WHERE " "username = :USER_NAME ");
477  deleteQuery.bindValue(":USER_NAME", username);
478 
479  bool bResult = deleteQuery.exec();
480  if (!bResult)
481  MythDB::DBError("Error removing digest user from database",
482  deleteQuery);
483 
484  return bResult;
485 }
486 
490 bool MythSessionManager::ChangeDigestUserPassword(const QString& username,
491  const QString& oldPassword,
492  const QString& newPassword)
493 {
494  if (newPassword.isEmpty())
495  {
496  LOG(VB_GENERAL, LOG_ERR, QString("New password is missing."));
497  return false;
498  }
499 
500  if (!IsValidUser(username))
501  {
502  LOG(VB_GENERAL, LOG_ERR, QString("Attempted to update non-existing"
503  " user: %1.").arg(username));
504  return false;
505  }
506 
507  QByteArray oldPasswordDigest = CreateDigest(username, oldPassword);
508 
509  if (oldPasswordDigest != GetPasswordDigest(username))
510  {
511  LOG(VB_GENERAL, LOG_ERR, QString("Incorrect old password for "
512  "user: %1.").arg(username));
513  return false;
514  }
515 
516  MSqlQuery update(MSqlQuery::InitCon());
517  update.prepare("UPDATE users SET "
518  "password_digest = :NEW_PASSWORD_DIGEST WHERE "
519  "username = :USER_NAME AND "
520  "password_digest = :OLD_PASSWORD_DIGEST");
521  update.bindValue(":NEW_PASSWORD_DIGEST", CreateDigest(username,
522  newPassword));
523  update.bindValue(":USER_NAME", username);
524  update.bindValue(":OLD_PASSWORD_DIGEST", oldPasswordDigest);
525 
526  bool bResult = update.exec();
527  if (!bResult)
528  MythDB::DBError("Error updating digest user in database", update);
529 
530  return bResult;
531 }
532 
536 QByteArray MythSessionManager::CreateDigest(const QString &username,
537  const QString &password)
538 {
539  // The realm is a constant, it's not private because it's included in the
540  // plain text sent with an HTTP WWW-Authenticate header.
541  QString plainText = QString("%1:MythTV:%2").arg(username, password);
542  QByteArray digest = QCryptographicHash::hash(plainText.toLatin1(),
543  QCryptographicHash::Md5).toHex();
544  return digest;
545 }
546 
551  const QString& username,
552  const QString& password,
553  const QString& newPassword,
554  const QString& adminPassword)
555 {
556  bool returnCode = false;
557 
558  if (username.isEmpty() || password.isEmpty())
559  LOG(VB_GENERAL, LOG_ERR, QString("Username and password required."));
560  else if (action == DIGEST_USER_ADD)
561  returnCode = AddDigestUser(username, password, adminPassword);
562  else if (action == DIGEST_USER_REMOVE)
563  returnCode = RemoveDigestUser(username, password);
564  else if (action == DIGEST_USER_CHANGE_PW)
565  returnCode = ChangeDigestUserPassword(username, password, newPassword);
566  else
567  LOG(VB_GENERAL, LOG_ERR, QString("Unknown action."));
568 
569  return returnCode;
570 }
MSqlQuery::next
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:812
MythSessionManager::IsValidUser
static bool IsValidUser(const QString &username)
Check if the given user exists but not whether there is a valid session open for them!
Definition: mythsession.cpp:151
MSqlQuery
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:127
MythSessionManager::LoadSessions
void LoadSessions(void)
Load the values from the sessions table on startup.
Definition: mythsession.cpp:120
MythSessionManager::GetPasswordDigest
static QString GetPasswordDigest(const QString &username)
Load the password digest for comparison in the HTTP Auth code.
Definition: mythsession.cpp:224
MythUserSession::m_userId
uint m_userId
Definition: mythsession.h:76
MSqlQuery::size
int size(void) const
Definition: mythdbcon.h:214
MythSessionManager::UpdateSession
void UpdateSession(const QString &sessionToken)
Update the session timestamps.
Definition: mythsession.cpp:269
MythSessionManager::m_sessionList
QMap< QString, MythUserSession > m_sessionList
Definition: mythsession.h:226
MSqlQuery::value
QVariant value(int i) const
Definition: mythdbcon.h:204
mythdbcon.h
MythUserSession::Update
bool Update(void)
Update session expiry and access times.
Definition: mythsession.cpp:101
MythSessionManager::DestroyUserSession
void DestroyUserSession(const QString &sessionToken)
Removes user session from the database and cache.
Definition: mythsession.cpp:388
MSqlQuery::exec
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:618
MythUserSession::m_name
QString m_name
Definition: mythsession.h:77
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MythSessionManager::CreateDigest
static QByteArray CreateDigest(const QString &username, const QString &password)
Generate a digest string.
Definition: mythsession.cpp:536
MythDate::current
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:14
MythUserSession
Definition: mythsession.h:18
MythUserSession::m_sessionLastActive
QDateTime m_sessionLastActive
Definition: mythsession.h:81
MythUserSession::m_sessionToken
QString m_sessionToken
Definition: mythsession.h:79
MythSessionManager::GetSession
MythUserSession GetSession(const QString &sessionToken)
Load the session details and return.
Definition: mythsession.cpp:175
MythCoreContext::IsMasterBackend
bool IsMasterBackend(void)
is this the actual MBE process
Definition: mythcorecontext.cpp:699
mythdate.h
DigestUserActions
DigestUserActions
Definition: mythsession.h:12
mythlogging.h
MythSessionManager::ManageDigestUser
static bool ManageDigestUser(DigestUserActions action, const QString &username, const QString &password, const QString &newPassword, const QString &adminPassword)
Manage digest user entries.
Definition: mythsession.cpp:550
MythUserSession::IsValid
bool IsValid(void) const
Check if this session object appears properly constructed, it DOES NOT validate whether it is a valid...
Definition: mythsession.cpp:15
MSqlQuery::InitCon
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:550
MythUserSession::m_sessionExpires
QDateTime m_sessionExpires
Definition: mythsession.h:82
MythDB::DBError
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:225
MythUserSession::Save
bool Save(void)
Save the session to the database.
Definition: mythsession.cpp:56
MythUserSession::CheckPermission
bool CheckPermission(const QString &context, uint permission)
Check if the user has the given permission in a context.
Definition: mythsession.cpp:37
uint
unsigned int uint
Definition: compat.h:81
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:55
MythSessionManager::LoginUser
MythUserSession LoginUser(const QString &username, const QByteArray &digest, const QString &client="")
Login user by digest.
Definition: mythsession.cpp:282
MythSessionManager::RemoveDigestUser
static bool RemoveDigestUser(const QString &username, const QString &password)
Definition: mythsession.cpp:451
DIGEST_USER_ADD
@ DIGEST_USER_ADD
Definition: mythsession.h:13
MythSessionManager::AddDigestUser
static bool AddDigestUser(const QString &username, const QString &password, const QString &adminPassword)
Definition: mythsession.cpp:410
MythSessionManager::ChangeDigestUserPassword
static bool ChangeDigestUserPassword(const QString &username, const QString &oldPassword, const QString &newPassword)
Definition: mythsession.cpp:490
mythcorecontext.h
MythUserSession::m_sessionCreated
QDateTime m_sessionCreated
Definition: mythsession.h:80
MythSessionManager::IsValidSession
bool IsValidSession(const QString &sessionToken)
Check if the session token is valid.
Definition: mythsession.cpp:242
DIGEST_USER_CHANGE_PW
@ DIGEST_USER_CHANGE_PW
Definition: mythsession.h:15
MSqlQuery::bindValue
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:888
MythSessionManager::CreateUserSession
MythUserSession CreateUserSession(uint userId, const QString &username, const QString &client)
Add new user session to the database and cache.
Definition: mythsession.cpp:351
MythUserSession::m_sessionClient
QString m_sessionClient
Definition: mythsession.h:83
build_compdb.action
action
Definition: build_compdb.py:9
MythCoreContext::GetHostName
QString GetHostName(void)
Definition: mythcorecontext.cpp:842
MythSessionManager::MythSessionManager
MythSessionManager()
Definition: mythsession.cpp:111
mythsession.h
DIGEST_USER_REMOVE
@ DIGEST_USER_REMOVE
Definition: mythsession.h:14
MSqlQuery::prepare
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:837