The Battle for Wesnoth  1.15.2+dev
forum_user_handler.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2018 by Thomas Baumhauer <thomas.baumhauer@NOSPAMgmail.com>
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
15 #ifdef HAVE_MYSQLPP
16 
19 #include "hash.hpp"
20 #include "log.hpp"
21 #include "config.hpp"
22 
23 #include <cstdlib>
24 #include <sstream>
25 
26 static lg::log_domain log_mp_user_handler("mp_user_handler");
27 #define ERR_UH LOG_STREAM(err, log_mp_user_handler)
28 #define WRN_UH LOG_STREAM(warn, log_mp_user_handler)
29 #define LOG_UH LOG_STREAM(info, log_mp_user_handler)
30 #define DBG_UH LOG_STREAM(debug, log_mp_user_handler)
31 
32 namespace {
33  const int USER_INACTIVE = 1;
34  const int USER_IGNORE = 2;
35 }
36 
37 fuh::fuh(const config& c)
38  : db_name_(c["db_name"].str())
39  , db_host_(c["db_host"].str())
40  , db_user_(c["db_user"].str())
41  , db_password_(c["db_password"].str())
42  , db_users_table_(c["db_users_table"].str())
43  , db_banlist_table_(c["db_banlist_table"].str())
44  , db_extra_table_(c["db_extra_table"].str())
45  , db_game_info_table_(c["db_game_info_table"].str())
46  , db_game_player_info_table_(c["db_game_player_info_table"].str())
47  , db_game_modification_info_table_(c["db_game_modification_info_table"].str())
48  , db_user_group_table_(c["db_user_group_table"].str())
49  , mp_mod_group_(0)
50  , conn(mysql_init(nullptr))
51 {
52  try {
53  mp_mod_group_ = std::stoi(c["mp_mod_group"].str());
54  } catch(...) {
55  ERR_UH << "Failed to convert the mp_mod_group value of '" << c["mp_mod_group"].str() << "' into an int! Defaulting to " << mp_mod_group_ << "." << std::endl;
56  }
57  mysql_options(conn, MYSQL_SET_CHARSET_NAME, "utf8mb4");
58  if(!conn || !mysql_real_connect(conn, db_host_.c_str(), db_user_.c_str(), db_password_.c_str(), db_name_.c_str(), 0, nullptr, 0)) {
59  ERR_UH << "Could not connect to database: " << mysql_errno(conn) << ": " << mysql_error(conn) << std::endl;
60  }
61 }
62 
63 fuh::~fuh() {
64  mysql_close(conn);
65 }
66 
67 // The hashing code is basically taken from forum_auth.cpp
68 bool fuh::login(const std::string& name, const std::string& password, const std::string& seed) {
69 
70  // Retrieve users' password as hash
71 
72  std::string hash;
73 
74  try {
75  hash = get_hash(name);
76  } catch (const error& e) {
77  ERR_UH << "Could not retrieve hash for user '" << name << "' :" << e.message << std::endl;
78  return false;
79  }
80 
81  std::string valid_hash;
82 
83  if(utils::md5::is_valid_hash(hash)) { // md5 hash
84  valid_hash = utils::md5(hash.substr(12,34), seed).base64_digest();
85  } else if(utils::bcrypt::is_valid_prefix(hash)) { // bcrypt hash
86  valid_hash = utils::md5(hash, seed).base64_digest();
87  } else {
88  ERR_UH << "Invalid hash for user '" << name << "'" << std::endl;
89  return false;
90  }
91 
92  if(password == valid_hash) return true;
93 
94  return false;
95 }
96 
97 std::string fuh::extract_salt(const std::string& name) {
98 
99  // Some double security, this should never be needed
100  if(!(user_exists(name))) {
101  return "";
102  }
103 
104  std::string hash;
105 
106  try {
107  hash = get_hash(name);
108  } catch (const error& e) {
109  ERR_UH << "Could not retrieve hash for user '" << name << "' :" << e.message << std::endl;
110  return "";
111  }
112 
113  if(utils::md5::is_valid_hash(hash))
114  return hash.substr(0,12);
115 
117  try {
119  } catch(const utils::hash_error& err) {
120  ERR_UH << "Error getting salt from hash of user '" << name << "': " << err.what() << std::endl;
121  return "";
122  }
123  }
124 
125  return "";
126 }
127 
128 void fuh::user_logged_in(const std::string& name) {
129  set_lastlogin(name, std::time(nullptr));
130 }
131 
132 bool fuh::user_exists(const std::string& name) {
133 
134  // Make a test query for this username
135  try {
136  return prepared_statement<bool>("SELECT 1 FROM `" + db_users_table_ + "` WHERE UPPER(username)=UPPER(?)", name);
137  } catch (const sql_error& e) {
138  ERR_UH << "Could not execute test query for user '" << name << "' :" << e.message << std::endl;
139  // If the database is down just let all usernames log in
140  return false;
141  }
142 }
143 
144 bool fuh::user_is_active(const std::string& name) {
145  try {
146  int user_type = get_detail_for_user<int>(name, "user_type");
147  return user_type != USER_INACTIVE && user_type != USER_IGNORE;
148  } catch (const sql_error& e) {
149  ERR_UH << "Could not retrieve user type for user '" << name << "' :" << e.message << std::endl;
150  return false;
151  }
152 }
153 
154 bool fuh::user_is_moderator(const std::string& name) {
155 
156  if(!user_exists(name)) return false;
157 
158  try {
159  return get_writable_detail_for_user<int>(name, "user_is_moderator") == 1 || (mp_mod_group_ != 0 && is_user_in_group(name, mp_mod_group_));
160  } catch (const sql_error& e) {
161  ERR_UH << "Could not query user_is_moderator/MP Moderators group for user '" << name << "' :" << e.message << std::endl;
162  // If the database is down mark nobody as a mod
163  return false;
164  }
165 }
166 
167 void fuh::set_is_moderator(const std::string& name, const bool& is_moderator) {
168 
169  if(!user_exists(name)) return;
170 
171  try {
172  write_detail(name, "user_is_moderator", static_cast<int>(is_moderator));
173  } catch (const sql_error& e) {
174  ERR_UH << "Could not set is_moderator for user '" << name << "' :" << e.message << std::endl;
175  }
176 }
177 
178 fuh::ban_info fuh::user_is_banned(const std::string& name, const std::string& addr)
179 {
180  //
181  // NOTE: glob IP and email address bans are NOT supported yet since they
182  // require a different kind of query that isn't supported by our
183  // prepared SQL statement API right now. However, they are basically
184  // never used on forums.wesnoth.org, so this shouldn't be a problem
185  // for the time being.
186  //
187 
188  // NOTE: A ban end time of 0 is a permanent ban.
189  const std::string& is_extant_ban_sql =
190  "ban_exclude = 0 AND (ban_end = 0 OR ban_end >=" + std::to_string(std::time(nullptr)) + ")";
191 
192  // TODO: retrieve full ban info in a single statement instead of issuing
193  // separate queries to check for a ban's existence and its duration.
194 
195  try {
196  if(!addr.empty() && prepared_statement<bool>("SELECT 1 FROM `" + db_banlist_table_ + "` WHERE UPPER(ban_ip) = UPPER(?) AND " + is_extant_ban_sql, addr)) {
197  LOG_UH << "User '" << name << "' ip " << addr << " banned by IP address\n";
198  return retrieve_ban_info(BAN_IP, addr);
199  }
200  } catch(const sql_error& e) {
201  ERR_UH << "Could not check forum bans on address '" << addr << "' :" << e.message << '\n';
202  return {};
203  }
204 
205  if(!user_exists(name)) return {};
206 
207  try {
208  auto uid = get_detail_for_user<unsigned int>(name, "user_id");
209 
210  if(uid == 0) {
211  ERR_UH << "Invalid user id for user '" << name << "'\n";
212  } else if(prepared_statement<bool>("SELECT 1 FROM `" + db_banlist_table_ + "` WHERE ban_userid = ? AND " + is_extant_ban_sql, uid)) {
213  LOG_UH << "User '" << name << "' uid " << uid << " banned by uid\n";
214  return retrieve_ban_info(BAN_USER, uid);
215  }
216 
217  auto email = get_detail_for_user<std::string>(name, "user_email");
218 
219  if(!email.empty() && prepared_statement<bool>("SELECT 1 FROM `" + db_banlist_table_ + "` WHERE UPPER(ban_email) = UPPER(?) AND " + is_extant_ban_sql, email)) {
220  LOG_UH << "User '" << name << "' email " << email << " banned by email address\n";
221  return retrieve_ban_info(BAN_EMAIL, email);
222  }
223 
224  } catch(const sql_error& e) {
225  ERR_UH << "Could not check forum bans on user '" << name << "' :" << e.message << '\n';
226  }
227 
228  return {};
229 }
230 
231 template<typename T>
233 {
234  std::string col;
235 
236  switch(type) {
237  case BAN_USER:
238  col = "ban_userid";
239  break;
240  case BAN_EMAIL:
241  col = "ban_email";
242  break;
243  case BAN_IP:
244  col = "ban_ip";
245  break;
246  default:
247  return {};
248  }
249 
250  try {
251  return { type, retrieve_ban_duration_internal(col, detail) };
252  } catch(const sql_error& e) {
253  //
254  // NOTE:
255  // If retrieve_ban_internal() fails to fetch the ban row, odds are the ban was
256  // lifted in the meantime (it's meant to be called by user_is_banned(), so we
257  // assume the ban expires in one second instead of returning 0 (permanent ban)
258  // just to err on the safe side (returning BAN_NONE would be a terrible idea,
259  // for that matter).
260  //
261  return { type, 1 };
262  }
263 }
264 
265 std::time_t fuh::retrieve_ban_duration_internal(const std::string& col, const std::string& detail)
266 {
267  const std::time_t end_time = prepared_statement<int>("SELECT `ban_end` FROM `" + db_banlist_table_ + "` WHERE UPPER(" + col + ") = UPPER(?)", detail);
268  return end_time ? end_time - std::time(nullptr) : 0;
269 }
270 
271 std::time_t fuh::retrieve_ban_duration_internal(const std::string& col, unsigned int detail)
272 {
273  const std::time_t end_time = prepared_statement<int>("SELECT `ban_end` FROM `" + db_banlist_table_ + "` WHERE " + col + " = ?", detail);
274  return end_time ? end_time - std::time(nullptr) : 0;
275 }
276 
277 std::string fuh::user_info(const std::string& name) {
278  if(!user_exists(name)) {
279  throw error("No user with the name '" + name + "' exists.");
280  }
281 
282  std::time_t reg_date = get_registrationdate(name);
283  std::time_t ll_date = get_lastlogin(name);
284 
285  std::string reg_string = ctime(&reg_date);
286  std::string ll_string;
287 
288  if(ll_date) {
289  ll_string = ctime(&ll_date);
290  } else {
291  ll_string = "Never\n";
292  }
293 
294  std::stringstream info;
295  info << "Name: " << name << "\n"
296  << "Registered: " << reg_string
297  << "Last login: " << ll_string;
298  if(!user_is_active(name)) {
299  info << "This account is currently inactive.\n";
300  }
301 
302  return info.str();
303 }
304 
305 std::string fuh::get_hash(const std::string& user) {
306  try {
307  return get_detail_for_user<std::string>(user, "user_password");
308  } catch (const sql_error& e) {
309  ERR_UH << "Could not retrieve password for user '" << user << "' :" << e.message << std::endl;
310  return "";
311  }
312 }
313 
314 std::time_t fuh::get_lastlogin(const std::string& user) {
315  try {
316  int time_int = get_writable_detail_for_user<int>(user, "user_lastvisit");
317  return std::time_t(time_int);
318  } catch (const sql_error& e) {
319  ERR_UH << "Could not retrieve last visit for user '" << user << "' :" << e.message << std::endl;
320  return std::time_t(0);
321  }
322 }
323 
324 std::time_t fuh::get_registrationdate(const std::string& user) {
325  try {
326  int time_int = get_detail_for_user<int>(user, "user_regdate");
327  return std::time_t(time_int);
328  } catch (const sql_error& e) {
329  ERR_UH << "Could not retrieve registration date for user '" << user << "' :" << e.message << std::endl;
330  return std::time_t(0);
331  }
332 }
333 
334 void fuh::set_lastlogin(const std::string& user, const std::time_t& lastlogin) {
335 
336  try {
337  write_detail(user, "user_lastvisit", static_cast<int>(lastlogin));
338  } catch (const sql_error& e) {
339  ERR_UH << "Could not set last visit for user '" << user << "' :" << e.message << std::endl;
340  }
341 }
342 
343 template<typename T, typename... Args>
344 inline T fuh::prepared_statement(const std::string& sql, Args&&... args)
345 {
346  try {
347  return ::prepared_statement<T>(conn, sql, std::forward<Args>(args)...);
348  } catch (const sql_error& e) {
349  WRN_UH << "caught sql error: " << e.message << std::endl;
350  WRN_UH << "trying to reconnect and retry..." << std::endl;
351  //Try to reconnect and execute query again
352  mysql_close(conn);
353  conn = mysql_init(nullptr);
354  mysql_options(conn, MYSQL_SET_CHARSET_NAME, "utf8mb4");
355  if(!conn || !mysql_real_connect(conn, db_host_.c_str(), db_user_.c_str(), db_password_.c_str(), db_name_.c_str(), 0, nullptr, 0)) {
356  ERR_UH << "Could not connect to database: " << mysql_errno(conn) << ": " << mysql_error(conn) << std::endl;
357  throw sql_error("Error querying database.");
358  }
359  }
360  return ::prepared_statement<T>(conn, sql, std::forward<Args>(args)...);
361 }
362 
363 template<typename T>
364 T fuh::get_detail_for_user(const std::string& name, const std::string& detail) {
365  return prepared_statement<T>(
366  "SELECT `" + detail + "` FROM `" + db_users_table_ + "` WHERE UPPER(username)=UPPER(?)",
367  name);
368 }
369 
370 template<typename T>
371 T fuh::get_writable_detail_for_user(const std::string& name, const std::string& detail) {
372  if(!extra_row_exists(name)) throw sql_error("row doesn't exist");
373  return prepared_statement<T>(
374  "SELECT `" + detail + "` FROM `" + db_extra_table_ + "` WHERE UPPER(username)=UPPER(?)",
375  name);
376 }
377 
378 template<typename T>
379 void fuh::write_detail(const std::string& name, const std::string& detail, T&& value) {
380  try {
381  // Check if we do already have a row for this user in the extra table
382  if(!extra_row_exists(name)) {
383  // If not create the row
384  prepared_statement<void>("INSERT INTO `" + db_extra_table_ + "` VALUES(?,?,'0')", name, std::forward<T>(value));
385  }
386  prepared_statement<void>("UPDATE `" + db_extra_table_ + "` SET " + detail + "=? WHERE UPPER(username)=UPPER(?)", std::forward<T>(value), name);
387  } catch (const sql_error& e) {
388  ERR_UH << "Could not set detail for user '" << name << "': " << e.message << std::endl;
389  }
390 }
391 
392 bool fuh::is_user_in_group(const std::string& name, unsigned int group_id) {
393  try {
394  return prepared_statement<bool>("SELECT 1 FROM `" + db_users_table_ + "` u, `" + db_user_group_table_ + "` ug WHERE UPPER(u.username)=UPPER(?) AND u.USER_ID = ug.USER_ID AND ug.GROUP_ID = ?", name, group_id);
395  } catch (const sql_error& e) {
396  ERR_UH << "Could not execute test query for user group '" << group_id << "' and username '" << name << "'" << e.message << std::endl;
397  return false;
398  }
399 }
400 
401 bool fuh::extra_row_exists(const std::string& name) {
402 
403  // Make a test query for this username
404  try {
405  return prepared_statement<bool>("SELECT 1 FROM `" + db_extra_table_ + "` WHERE UPPER(username)=UPPER(?)", name);
406  } catch (const sql_error& e) {
407  ERR_UH << "Could not execute test query for user '" << name << "' :" << e.message << std::endl;
408  return false;
409  }
410 }
411 
412 std::string fuh::get_uuid(){
413  try {
414  return prepared_statement<std::string>("SELECT UUID()");
415  } catch (const sql_error& e) {
416  ERR_UH << "Could not retrieve a UUID:" << e.message << std::endl;
417  return "";
418  }
419 }
420 
421 void fuh::db_insert_game_info(const std::string& uuid, int game_id, const std::string& version, const std::string& name){
422  try {
423  prepared_statement<void>("INSERT INTO `" + db_game_info_table_ + "`(INSTANCE_UUID, GAME_ID, INSTANCE_VERSION, GAME_NAME) VALUES(?, ?, ?, ?)",
424  uuid, game_id, version, name);
425  } catch (const sql_error& e) {
426  ERR_UH << "Could not insert into table `" + db_game_info_table_ + "`:" << e.message << std::endl;
427  }
428 }
429 
430 void fuh::db_update_game_start(const std::string& uuid, int game_id, const std::string& map_name, const std::string& era_name, int reload, int observers, int is_public, int has_password){
431  try {
432  prepared_statement<void>("UPDATE `" + db_game_info_table_ + "` SET START_TIME = CURRENT_TIMESTAMP, MAP_NAME = ?, ERA_NAME = ?, RELOAD = ?, OBSERVERS = ?, PUBLIC = ?, PASSWORD = ? WHERE INSTANCE_UUID = ? AND GAME_ID = ?",
433  map_name, era_name, reload, observers, is_public, has_password, uuid, game_id);
434  } catch (const sql_error& e) {
435  ERR_UH << "Could not update the game's starting information on table `" + db_game_info_table_ + "`:" << e.message << std::endl;
436  }
437 }
438 
439 void fuh::db_update_game_end(const std::string& uuid, int game_id, const std::string& replay_location){
440  try {
441  prepared_statement<void>("UPDATE `" + db_game_info_table_ + "` SET END_TIME = CURRENT_TIMESTAMP, REPLAY_NAME = ? WHERE INSTANCE_UUID = ? AND GAME_ID = ?",
442  replay_location, uuid, game_id);
443  } catch (const sql_error& e) {
444  ERR_UH << "Could not update the game's ending information on table `" + db_game_info_table_ + "`:" << e.message << std::endl;
445  }
446 }
447 
448 void fuh::db_insert_game_player_info(const std::string& uuid, int game_id, const std::string& username, int side_number, int is_host, const std::string& faction, const std::string& version, const std::string& source, const std::string& current_user){
449  try {
450  prepared_statement<void>("INSERT INTO `" + db_game_player_info_table_ + "`(INSTANCE_UUID, GAME_ID, USER_ID, SIDE_NUMBER, IS_HOST, FACTION, CLIENT_VERSION, CLIENT_SOURCE, USER_NAME) VALUES(?, ?, IFNULL((SELECT user_id FROM `"+db_users_table_+"` WHERE username = ?), -1), ?, ?, ?, ?, ?, ?)",
451  uuid, game_id, username, side_number, is_host, faction, version, source, current_user);
452  } catch (const sql_error& e) {
453  ERR_UH << "Could not insert the game's player information on table `" + db_game_player_info_table_ + "`:" << e.message << std::endl;
454  }
455 }
456 
457 void fuh::db_insert_modification_info(const std::string& uuid, int game_id, const std::string& modification_name){
458  try {
459  prepared_statement<void>("INSERT INTO `" + db_game_modification_info_table_ + "`(INSTANCE_UUID, GAME_ID, MODIFICATION_NAME) VALUES(?, ?, ?)",
460  uuid, game_id, modification_name);
461  } catch (const sql_error& e) {
462  ERR_UH << "Could not insert the game's modification information on table `" + db_game_modification_info_table_ + "`:" << e.message << std::endl;
463  }
464 }
465 
466 void fuh::db_set_oos_flag(const std::string& uuid, int game_id){
467  try {
468  prepared_statement<void>("UPDATE `" + db_game_info_table_ + "` SET OOS = 1 WHERE INSTANCE_UUID = ? AND GAME_ID = ?",
469  uuid, game_id);
470  } catch (const sql_error& e) {
471  ERR_UH << "Could not update the game's OOS flag on table `" + db_game_info_table_ + "`:" << e.message << std::endl;
472  }
473 }
474 
475 #endif //HAVE_MYSQLPP
std::string get_hash(const std::string &user)
virtual std::string base64_digest() const override
Definition: hash.cpp:121
static l_noret error(LoadState *S, const char *why)
Definition: lundump.cpp:39
logger & info()
Definition: log.cpp:90
std::string get_uuid()
static bool is_valid_prefix(const std::string &hash)
Definition: hash.cpp:187
void user_logged_in(const std::string &name)
Executed when the user with the given name logged in.
T prepared_statement(const std::string &sql, Args &&...)
std::time_t get_lastlogin(const std::string &user)
std::string user_info(const std::string &name)
Returns a string containing info like the last login of this user.
ban_info retrieve_ban_info(BAN_TYPE, T detail)
bool user_exists(const std::string &name)
Returns true if a user with the given name exists.
void set_is_moderator(const std::string &name, const bool &is_moderator)
Mark this user as a moderator.
fuh(const config &c)
T get_detail_for_user(const std::string &name, const std::string &detail)
bool is_user_in_group(const std::string &name, unsigned int group_id)
static bool is_valid_hash(const std::string &hash)
Definition: hash.cpp:96
bool extra_row_exists(const std::string &name)
ban_info user_is_banned(const std::string &name, const std::string &addr)
Returns true if this user account or IP address is banned.
void db_insert_modification_info(const std::string &uuid, int game_id, const std::string &modification_name)
const char * what() const noexcept
Definition: exceptions.hpp:37
bool login(const std::string &name, const std::string &password, const std::string &seed)
Return true if the given password matches the password for the given user.
void db_set_oos_flag(const std::string &uuid, int game_id)
logger & err()
Definition: log.cpp:78
void db_insert_game_info(const std::string &uuid, int game_id, const std::string &version, const std::string &name)
bool user_is_active(const std::string &name)
Returns true if the specified user account is usable for logins.
Ban status description.
std::string password(const std::string &server, const std::string &login)
static bcrypt from_hash_string(const std::string &input)
Definition: hash.cpp:168
std::time_t get_registrationdate(const std::string &user)
~fuh()
bool user_is_moderator(const std::string &name)
Returns true if this user is a moderator on this server.
void write_detail(const std::string &name, const std::string &detail, T &&value)
void db_update_game_start(const std::string &uuid, int game_id, const std::string &map_name, const std::string &era_name, int reload, int observers, int is_public, int has_password)
std::time_t retrieve_ban_duration_internal(const std::string &col, const std::string &detail)
T get_writable_detail_for_user(const std::string &name, const std::string &detail)
std::string message
Definition: exceptions.hpp:31
void set_lastlogin(const std::string &user, const std::time_t &lastlogin)
BAN_TYPE
Ban type values.
#define e
std::string get_salt() const
Definition: hash.cpp:194
int side_number
Definition: game_info.hpp:39
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:68
std::string extract_salt(const std::string &name)
Needed because the hashing algorithm used by phpbb requires some info from the original hash to recre...
mock_char c
void db_update_game_end(const std::string &uuid, int game_id, const std::string &replay_location)
void db_insert_game_player_info(const std::string &uuid, int game_id, const std::string &username, int side_number, int is_host, const std::string &faction, const std::string &version, const std::string &source, const std::string &current_user)