The Battle for Wesnoth  1.19.12+dev
server.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 /**
17  * @file
18  * Wesnoth-Server, for multiplayer-games.
19  */
20 
22 
23 #include "config.hpp"
24 #include "filesystem.hpp"
25 #include "log.hpp"
27 #include "serialization/chrono.hpp"
28 #include "serialization/parser.hpp"
32 #include "utils/iterable_pair.hpp"
33 #include "game_version.hpp"
34 
35 #include "server/wesnothd/ban.hpp"
36 #include "server/wesnothd/game.hpp"
42 
43 #ifdef HAVE_MYSQLPP
45 #endif
46 
47 #include <boost/algorithm/string.hpp>
48 #include <boost/scope_exit.hpp>
49 
50 #include <algorithm>
51 #include <cassert>
52 #include <cerrno>
53 #include <cstdlib>
54 #include <functional>
55 #include <iostream>
56 #include <map>
57 #include <set>
58 #include <sstream>
59 #include <utility>
60 #include <vector>
61 
62 static lg::log_domain log_server("server");
63 /**
64  * fatal and directly server related errors/warnings,
65  * ie not caused by erroneous client data
66  */
67 #define ERR_SERVER LOG_STREAM(err, log_server)
68 
69 /** clients send wrong/unexpected data */
70 #define WRN_SERVER LOG_STREAM(warn, log_server)
71 
72 /** normal events */
73 #define LOG_SERVER LOG_STREAM(info, log_server)
74 #define DBG_SERVER LOG_STREAM(debug, log_server)
75 
76 static lg::log_domain log_config("config");
77 #define ERR_CONFIG LOG_STREAM(err, log_config)
78 #define WRN_CONFIG LOG_STREAM(warn, log_config)
79 
80 using namespace std::chrono_literals;
81 
82 namespace wesnothd
83 {
84 // we take profiling info on every n requests
87 
88 static void make_add_diff(
89  const simple_wml::node& src, const char* gamelist, const char* type, simple_wml::document& out, int index = -1)
90 {
91  if(!out.child("gamelist_diff")) {
92  out.root().add_child("gamelist_diff");
93  }
94 
95  simple_wml::node* top = out.child("gamelist_diff");
96  if(gamelist) {
97  top = &top->add_child("change_child");
98  top->set_attr_int("index", 0);
99  top = &top->add_child("gamelist");
100  }
101 
102  simple_wml::node& insert = top->add_child("insert_child");
103  const simple_wml::node::child_list& children = src.children(type);
104  assert(!children.empty());
105 
106  if(index < 0) {
107  index = children.size() - 1;
108  }
109 
110  assert(index < static_cast<int>(children.size()));
111  insert.set_attr_int("index", index);
112 
113  children[index]->copy_into(insert.add_child(type));
114 }
115 
117  const char* gamelist,
118  const char* type,
119  const simple_wml::node* remove,
121 {
122  if(!out.child("gamelist_diff")) {
123  out.root().add_child("gamelist_diff");
124  }
125 
126  simple_wml::node* top = out.child("gamelist_diff");
127  if(gamelist) {
128  top = &top->add_child("change_child");
129  top->set_attr_int("index", 0);
130  top = &top->add_child("gamelist");
131  }
132 
133  const simple_wml::node::child_list& children = src.children(type);
134  const auto itor = std::find(children.begin(), children.end(), remove);
135 
136  if(itor == children.end()) {
137  return false;
138  }
139 
140  const int index = std::distance(children.begin(), itor);
141 
142  simple_wml::node& del = top->add_child("delete_child");
143  del.set_attr_int("index", index);
144  del.add_child(type);
145 
146  return true;
147 }
148 
150  const char* gamelist,
151  const char* type,
152  const simple_wml::node* item,
154 {
155  if(!out.child("gamelist_diff")) {
156  out.root().add_child("gamelist_diff");
157  }
158 
159  simple_wml::node* top = out.child("gamelist_diff");
160  if(gamelist) {
161  top = &top->add_child("change_child");
162  top->set_attr_int("index", 0);
163  top = &top->add_child("gamelist");
164  }
165 
166  const simple_wml::node::child_list& children = src.children(type);
167  const auto itor = std::find(children.begin(), children.end(), item);
168 
169  if(itor == children.end()) {
170  return false;
171  }
172 
173  simple_wml::node& diff = *top;
174  simple_wml::node& del = diff.add_child("delete_child");
175 
176  const int index = std::distance(children.begin(), itor);
177 
178  del.set_attr_int("index", index);
179  del.add_child(type);
180 
181  // inserts will be processed first by the client, so insert at index+1,
182  // and then when the delete is processed we'll slide into the right position
183  simple_wml::node& insert = diff.add_child("insert_child");
184  insert.set_attr_int("index", index + 1);
185 
186  children[index]->copy_into(insert.add_child(type));
187  return true;
188 }
189 
190 static std::string player_status(const wesnothd::player_record& player)
191 {
193  std::ostringstream out;
194  out << "'" << player.name() << "' @ " << player.client_ip()
195  << " logged on for "
196  << d.count() << " days, "
197  << h.count() << " hours, "
198  << m.count() << " minutes, "
199  << s.count() << " seconds";
200  return out.str();
201 }
202 
203 const std::string denied_msg = "You're not allowed to execute this command.";
204 const std::string help_msg =
205  "Available commands are: adminmsg <msg>,"
206  " ban <mask> <time> <reason>, bans [deleted] [<ipmask>], clones,"
207  " dul|deny_unregistered_login [yes|no], kick <mask> [<reason>],"
208  " k[ick]ban <mask> <time> <reason>, help, games, metrics,"
209  " [lobby]msg <message>, motd [<message>],"
210  " pm|privatemsg <nickname> <message>, requests, roll <sides>, sample, searchlog <mask>,"
211  " signout, stats, status [<mask>], stopgame <nick> [<reason>], reset_queues, unban <ipmask>\n"
212  "Specific strings (those not in between <> like the command names)"
213  " are case insensitive.";
214 
215 server::server(int port,
216  bool keep_alive,
217  const std::string& config_file)
218  : server_base(port, keep_alive)
219  , ban_manager_()
220  , ip_log_()
221  , failed_logins_()
222  , user_handler_(nullptr)
223  , die_(static_cast<unsigned>(std::time(nullptr)))
224 #ifndef _WIN32
225  , input_path_()
226 #endif
227  , uuid_("")
228  , config_file_(config_file)
229  , cfg_(read_config())
230  , accepted_versions_()
231  , redirected_versions_()
232  , proxy_versions_()
233  , disallowed_names_()
234  , admin_passwd_()
235  , motd_()
236  , announcements_()
237  , server_id_()
238  , tournaments_()
239  , information_()
240  , default_max_messages_(0)
241  , default_time_period_(0)
242  , concurrent_connections_(0)
243  , graceful_restart(false)
244  , lan_server_(0)
245  , restart_command()
246  , max_ip_log_size_(0)
247  , deny_unregistered_login_(false)
248  , save_replays_(false)
249  , replay_save_path_()
250  , allow_remote_shutdown_(false)
251  , client_sources_()
252  , tor_ip_list_()
253  , failed_login_limit_()
254  , failed_login_ban_()
255  , failed_login_buffer_size_()
256  , version_query_response_("[version]\n[/version]\n", simple_wml::INIT_COMPRESSED)
257  , login_response_("[mustlogin]\n[/mustlogin]\n", simple_wml::INIT_COMPRESSED)
258  , games_and_users_list_("[gamelist]\n[/gamelist]\n", simple_wml::INIT_STATIC)
259  , metrics_()
260  , dump_stats_timer_(io_service_)
261  , tournaments_timer_(io_service_)
262  , cmd_handlers_()
263  , timer_(io_service_)
264  , lan_server_timer_(io_service_)
265  , dummy_player_timer_(io_service_)
266  , dummy_player_timer_interval_(30)
267 {
268  setup_handlers();
269  load_config(false);
270  ban_manager_.read();
271 
272  start_server();
273 
276 }
277 
278 #ifndef _WIN32
279 void server::handle_sighup(const boost::system::error_code& error, int)
280 {
281  assert(!error);
282 
283  WRN_SERVER << "SIGHUP caught, reloading config";
284 
285  cfg_ = read_config();
286  load_config(true);
287 
288  sighup_.async_wait(std::bind(&server::handle_sighup, this, std::placeholders::_1, std::placeholders::_2));
289 }
290 #endif
291 
292 void server::handle_graceful_timeout(const boost::system::error_code& error)
293 {
294  assert(!error);
295 
296  if(games().empty()) {
297  process_command("msg All games ended. Shutting down now. Reconnect to the new server instance.", "system");
298  BOOST_THROW_EXCEPTION(server_shutdown("graceful shutdown timeout"));
299  } else {
300  timer_.expires_after(1s);
301  timer_.async_wait(std::bind(&server::handle_graceful_timeout, this, std::placeholders::_1));
302  }
303 }
304 
306 {
307  lan_server_timer_.expires_after(lan_server_);
308  lan_server_timer_.async_wait([this](const boost::system::error_code& ec) { handle_lan_server_shutdown(ec); });
309 }
310 
312 {
313  lan_server_timer_.cancel();
314 }
315 
316 void server::handle_lan_server_shutdown(const boost::system::error_code& error)
317 {
318  if(error)
319  return;
320 
321  BOOST_THROW_EXCEPTION(server_shutdown("lan server shutdown"));
322 }
323 
325 {
326 #ifndef _WIN32
327  const int res = mkfifo(input_path_.c_str(), 0660);
328  if(res != 0 && errno != EEXIST) {
329  ERR_SERVER << "could not make fifo at '" << input_path_ << "' (" << strerror(errno) << ")";
330  return;
331  }
332  int fifo = open(input_path_.c_str(), O_RDWR | O_NONBLOCK);
333  if(fifo == -1) {
334  ERR_SERVER << "could not open fifo at '" << input_path_ << "' (" << strerror(errno) << ")";
335  return;
336  }
337  input_.assign(fifo);
338  LOG_SERVER << "opened fifo at '" << input_path_ << "'. Server commands may be written to this file.";
339  read_from_fifo();
340 #endif
341 }
342 
343 #ifndef _WIN32
344 
345 void server::handle_read_from_fifo(const boost::system::error_code& error, std::size_t)
346 {
347  if(error) {
348  std::cout << error.message() << std::endl;
349  return;
350  }
351 
352  std::istream is(&admin_cmd_);
353  std::string cmd;
354  std::getline(is, cmd);
355 
356  LOG_SERVER << "Admin Command: type: " << cmd;
357 
358  const std::string res = process_command(cmd, "*socket*");
359 
360  // Only mark the response if we fake the issuer (i.e. command comes from IRC or so)
361  if(!cmd.empty() && cmd.at(0) == '+') {
362  LOG_SERVER << "[admin_command_response]\n"
363  << res << "\n"
364  << "[/admin_command_response]";
365  } else {
366  LOG_SERVER << res;
367  }
368 
369  read_from_fifo();
370 }
371 
372 #endif
373 
375 {
376 #define SETUP_HANDLER(name, function) \
377  cmd_handlers_[name] = std::bind(function, this, \
378  std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4);
379 
394  SETUP_HANDLER("privatemsg", &server::pm_handler);
396  SETUP_HANDLER("lobbymsg", &server::msg_handler);
411  SETUP_HANDLER("deny_unregistered_login", &server::dul_handler);
414 
415 #undef SETUP_HANDLER
416 }
417 
419 {
420  if(config_file_.empty()) {
421  return {};
422  }
423 
424  try {
425  // necessary to avoid assert since preprocess_file() goes through filesystem::get_short_wml_path()
426  filesystem::set_user_data_dir(std::string());
428  LOG_SERVER << "Server configuration from file: '" << config_file_ << "' read.";
429  } catch(const config::error& e) {
430  ERR_CONFIG << "ERROR: Could not read configuration file: '" << config_file_ << "': '" << e.message << "'.";
431  return {};
432  }
433 }
434 
435 void server::load_config(bool reload)
436 {
437 #ifndef _WIN32
438 #ifndef FIFODIR
439 #warning No FIFODIR set
440 #define FIFODIR "/var/run/wesnothd"
441 #endif
442  const std::string fifo_path
443  = (cfg_["fifo_path"].empty() ? std::string(FIFODIR) + "/socket" : std::string(cfg_["fifo_path"]));
444  // Reset (replace) the input stream only if the FIFO path changed.
445  if(fifo_path != input_path_) {
446  input_.close();
447  input_path_ = fifo_path;
448  setup_fifo();
449  }
450 #endif
451 
452  save_replays_ = cfg_["save_replays"].to_bool();
453  replay_save_path_ = cfg_["replay_save_path"].str();
454 
455  tor_ip_list_ = utils::split(cfg_["tor_ip_list_path"].empty()
456  ? ""
457  : filesystem::read_file(cfg_["tor_ip_list_path"]), '\n');
458 
459  // mp tests script doesn't have a config at all, so this child won't be here
460  // LAN also presumably wouldn't have these
461  if(!reload && cfg_.has_child("queues")) {
462  queue_info_.clear();
463  for(const config& queue : cfg_.mandatory_child("queues").child_range("queue")) {
464  const config& game = queue.mandatory_child("game");
465  queue_info q = queue_info(queue["id"].to_int(), game["scenario"].str(), queue["display_name"].str(), queue["players_required"].to_int(), game);
466  queue_info_.emplace(q.id, q);
467  }
468  } else if(reload && cfg_.has_child("queues")) {
469  std::map<int, queue_info> new_queue_info;
470  for(const config& queue : cfg_.mandatory_child("queues").child_range("queue")) {
471  const config& game = queue.mandatory_child("game");
472  queue_info q = queue_info(queue["id"].to_int(), game["scenario"].str(), queue["display_name"].str(), queue["players_required"].to_int(), game);
473  new_queue_info.emplace(q.id, q);
474  }
475 
476  // check for new or updated queues
477  for(auto& [id, info] : new_queue_info) {
478  if(queue_info_.count(id) == 0) {
479  simple_wml::document queue_update;
480  simple_wml::node& update = queue_update.root().add_child("queue_update");
481  update.set_attr_int("queue_id", info.id);
482  update.set_attr_dup("action", "add");
483  update.set_attr_dup("scenario_id", info.scenario_id.c_str());
484  update.set_attr_dup("display_name", info.display_name.c_str());
485  update.set_attr_int("players_required", info.players_required);
486 
487  send_to_lobby(queue_update);
488  } else if(
489  queue_info_.count(id) == 1 && (
490  info.scenario_id != queue_info_.at(id).scenario_id ||
491  info.display_name != queue_info_.at(id).display_name ||
492  info.players_required != queue_info_.at(id).players_required
493  )
494  ) {
495  simple_wml::document queue_update;
496  simple_wml::node& update = queue_update.root().add_child("queue_update");
497  update.set_attr_int("queue_id", info.id);
498  update.set_attr_dup("action", "update");
499  update.set_attr_dup("scenario_id", info.scenario_id.c_str());
500  update.set_attr_dup("display_name", info.display_name.c_str());
501  update.set_attr_int("players_required", info.players_required);
502 
503  send_to_lobby(queue_update);
504  }
505  }
506 
507  // check for removed queues
508  for(auto& [id, info] : queue_info_) {
509  if(new_queue_info.count(id) == 0) {
510  simple_wml::document queue_update;
511  simple_wml::node& update = queue_update.root().add_child("queue_update");
512  update.set_attr_int("queue_id", info.id);
513  update.set_attr_dup("action", "remove");
514 
515  send_to_lobby(queue_update);
516  }
517  }
518 
519  queue_info_ = new_queue_info;
520  }
521 
522  admin_passwd_ = cfg_["passwd"].str();
523  motd_ = cfg_["motd"].str();
524  information_ = cfg_["information"].str();
525  announcements_ = cfg_["announcements"].str();
526  server_id_ = cfg_["id"].str();
527  lan_server_ = chrono::parse_duration(cfg_["lan_server"], 0s);
528 
529  deny_unregistered_login_ = cfg_["deny_unregistered_login"].to_bool();
530 
531  allow_remote_shutdown_ = cfg_["allow_remote_shutdown"].to_bool();
532 
533  for(const std::string& source : utils::split(cfg_["client_sources"].str())) {
534  client_sources_.insert(source);
535  }
536 
537  disallowed_names_.clear();
538  if(cfg_["disallow_names"].empty()) {
539  disallowed_names_.push_back("*admin*");
540  disallowed_names_.push_back("*admln*");
541  disallowed_names_.push_back("*server*");
542  disallowed_names_.push_back("player");
543  disallowed_names_.push_back("network");
544  disallowed_names_.push_back("human");
545  disallowed_names_.push_back("computer");
546  disallowed_names_.push_back("ai");
547  disallowed_names_.push_back("ai?");
548  disallowed_names_.push_back("*moderator*");
549  } else {
550  disallowed_names_ = utils::split(cfg_["disallow_names"]);
551  }
552 
553  default_max_messages_ = cfg_["max_messages"].to_int(4);
554  default_time_period_ = chrono::parse_duration(cfg_["messages_time_period"], 10s);
555  concurrent_connections_ = cfg_["connections_allowed"].to_int(5);
556  max_ip_log_size_ = cfg_["max_ip_log_size"].to_int(500);
557 
558  failed_login_limit_ = cfg_["failed_logins_limit"].to_int(10);
559  failed_login_ban_ = chrono::parse_duration(cfg_["failed_logins_ban"], 3600s);
560  failed_login_buffer_size_ = cfg_["failed_logins_buffer_size"].to_int(500);
561 
562  // Example config line:
563  // restart_command="./wesnothd-debug -d -c ~/.wesnoth1.5/server.cfg"
564  // remember to make new one as a daemon or it will block old one
565  restart_command = cfg_["restart_command"].str();
566 
567  recommended_version_ = cfg_["recommended_version"].str();
568  accepted_versions_.clear();
569  const std::string& versions = cfg_["versions_accepted"];
570  if(versions.empty() == false) {
571  accepted_versions_ = utils::split(versions);
572  } else {
574  accepted_versions_.push_back("test");
575  }
576 
577  redirected_versions_.clear();
578  for(const config& redirect : cfg_.child_range("redirect")) {
579  for(const std::string& version : utils::split(redirect["version"])) {
580  redirected_versions_[version] = redirect;
581  }
582  }
583 
584  proxy_versions_.clear();
585  for(const config& proxy : cfg_.child_range("proxy")) {
586  for(const std::string& version : utils::split(proxy["version"])) {
587  proxy_versions_[version] = proxy;
588  }
589  }
590 
592 
593  // If there is a [user_handler] tag in the config file
594  // allow nick registration, otherwise we set user_handler_
595  // to nullptr. Thus we must check user_handler_ for not being
596  // nullptr every time we want to use it.
597  user_handler_.reset();
598 
599 #ifdef HAVE_MYSQLPP
600  if(auto user_handler = cfg_.optional_child("user_handler")) {
601  if(server_id_ == "") {
602  ERR_SERVER << "The server id must be set when database support is used";
603  exit(1);
604  }
605 
606  user_handler_.reset(new fuh(*user_handler));
607  uuid_ = user_handler_->get_uuid();
608  tournaments_ = user_handler_->get_tournaments();
609  }
610 #endif
611 
613 
614  if(cfg_["dummy_player_count"].to_int() > 0) {
615  for(int i = 0; i < cfg_["dummy_player_count"].to_int(); i++) {
616  simple_wml::node& dummy_user = games_and_users_list_.root().add_child_at("user", i);
617  dummy_user.set_attr_dup("available", "yes");
618  dummy_user.set_attr_int("forum_id", i);
619  dummy_user.set_attr_int("game_id", 0);
620  dummy_user.set_attr_dup("location", "");
621  dummy_user.set_attr_dup("moderator", "no");
622  dummy_user.set_attr_dup("name", ("player"+std::to_string(i)).c_str());
623  dummy_user.set_attr_dup("registered", "yes");
624  dummy_user.set_attr_dup("status", "lobby");
625  }
626  if(cfg_["dummy_player_timer_interval"].to_int() > 0) {
627  dummy_player_timer_interval_ = chrono::parse_duration(cfg_["dummy_player_timer_interval"], 0s);
628  }
630  }
631 }
632 
633 bool server::ip_exceeds_connection_limit(const std::string& ip) const
634 {
635  if(concurrent_connections_ == 0) {
636  return false;
637  }
638 
639  std::size_t connections = 0;
640  for(const auto& player : player_connections_) {
641  if(player.client_ip() == ip) {
642  ++connections;
643  }
644  }
645 
646  return connections >= concurrent_connections_;
647 }
648 
649 utils::optional<server_base::login_ban_info> server::is_ip_banned(const std::string& ip)
650 {
651  if(utils::contains(tor_ip_list_, ip)) {
652  return login_ban_info{ MP_SERVER_IP_BAN_ERROR, "TOR IP", {} };
653  }
654 
655  if(auto server_ban_info = ban_manager_.get_ban_info(ip)) {
656  return login_ban_info{
658  server_ban_info->get_reason(),
659  server_ban_info->get_remaining_ban_time()
660  };
661  }
662 
663  return {};
664 }
665 
667 {
668  dump_stats_timer_.expires_after(5min);
669  dump_stats_timer_.async_wait([this](const boost::system::error_code& ec) { dump_stats(ec); });
670 }
671 
672 void server::dump_stats(const boost::system::error_code& ec)
673 {
674  if(ec) {
675  ERR_SERVER << "Error waiting for dump stats timer: " << ec.message();
676  return;
677  }
678  LOG_SERVER << "Statistics:"
679  << "\tnumber_of_games = " << games().size()
680  << "\tnumber_of_users = " << player_connections_.size();
682 }
683 
685 {
687  dummy_player_timer_.async_wait([this](const boost::system::error_code& ec) { dummy_player_updates(ec); });
688 }
689 
690 void server::dummy_player_updates(const boost::system::error_code& ec)
691 {
692  if(ec) {
693  ERR_SERVER << "Error waiting for dummy player timer: " << ec.message();
694  return;
695  }
696 
697  int size = games_and_users_list_.root().children("user").size();
698  LOG_SERVER << "player count: " << size;
699  if(size % 2 == 0) {
700  simple_wml::node* dummy_user = games_and_users_list_.root().children("user").at(size-1);
701 
703  if(make_delete_diff(games_and_users_list_.root(), nullptr, "user", dummy_user, diff)) {
704  send_to_lobby(diff);
705  }
706 
708  } else {
709  simple_wml::node& dummy_user = games_and_users_list_.root().add_child_at("user", size-1);
710  dummy_user.set_attr_dup("available", "yes");
711  dummy_user.set_attr_int("forum_id", size-1);
712  dummy_user.set_attr_int("game_id", 0);
713  dummy_user.set_attr_dup("location", "");
714  dummy_user.set_attr_dup("moderator", "no");
715  dummy_user.set_attr_dup("name", ("player"+std::to_string(size-1)).c_str());
716  dummy_user.set_attr_dup("registered", "yes");
717  dummy_user.set_attr_dup("status", "lobby");
718 
720  make_add_diff(games_and_users_list_.root(), nullptr, "user", diff);
721  send_to_lobby(diff);
722  }
723 
725 }
726 
728 {
729  tournaments_timer_.expires_after(60min);
730  tournaments_timer_.async_wait([this](const boost::system::error_code& ec) { refresh_tournaments(ec); });
731 }
732 
733 void server::refresh_tournaments(const boost::system::error_code& ec)
734 {
735  if(ec) {
736  ERR_SERVER << "Error waiting for tournament refresh timer: " << ec.message();
737  return;
738  }
739  if(user_handler_) {
740  tournaments_ = user_handler_->get_tournaments();
742  }
743 }
744 
746 {
747  boost::asio::spawn(io_service_, [socket, this](boost::asio::yield_context yield) { login_client(std::move(yield), socket); }
748 #if BOOST_VERSION >= 108000
749  , [](const std::exception_ptr& e) { if (e) std::rethrow_exception(e); }
750 #endif
751  );
752 }
753 
755 {
756  boost::asio::spawn(io_service_, [socket, this](boost::asio::yield_context yield) { login_client(std::move(yield), socket); }
757 #if BOOST_VERSION >= 108000
758  , [](const std::exception_ptr& e) { if (e) std::rethrow_exception(e); }
759 #endif
760  );
761 }
762 
763 template<class SocketPtr>
764 void server::login_client(boost::asio::yield_context yield, SocketPtr socket)
765 {
766  coro_send_doc(socket, version_query_response_, yield);
767 
768  auto doc { coro_receive_doc(socket, yield) };
769  if(!doc) return;
770 
771  std::string client_version, client_source;
772  if(const simple_wml::node* const version = doc->child("version")) {
773  const simple_wml::string_span& version_str_span = (*version)["version"];
774  client_version = std::string { version_str_span.begin(), version_str_span.end() };
775 
776  const simple_wml::string_span& source_str_span = (*version)["client_source"];
777  client_source = std::string { source_str_span.begin(), source_str_span.end() };
778 
779  // Check if it is an accepted version.
780  auto accepted_it = std::find_if(accepted_versions_.begin(), accepted_versions_.end(),
781  std::bind(&utils::wildcard_string_match, client_version, std::placeholders::_1));
782 
783  if(accepted_it != accepted_versions_.end()) {
784  LOG_SERVER << log_address(socket) << "\tplayer joined using accepted version " << client_version
785  << ":\ttelling them to log in.";
786  coro_send_doc(socket, login_response_, yield);
787  } else {
788  simple_wml::document response;
789 
790  // Check if it is a redirected version
791  for(const auto& redirect_version : redirected_versions_) {
792  if(utils::wildcard_string_match(client_version, redirect_version.first)) {
793  LOG_SERVER << log_address(socket) << "\tplayer joined using version " << client_version
794  << ":\tredirecting them to " << redirect_version.second["host"] << ":"
795  << redirect_version.second["port"];
796 
797  simple_wml::node& redirect = response.root().add_child("redirect");
798  for(const auto& attr : redirect_version.second.attribute_range()) {
799  redirect.set_attr_dup(attr.first.c_str(), attr.second.str().c_str());
800  }
801 
802  async_send_doc_queued(socket, response);
803  return;
804  }
805  }
806 
807  LOG_SERVER << log_address(socket) << "\tplayer joined using unknown version " << client_version
808  << ":\trejecting them";
809 
810  // For compatibility with older clients
811  response.set_attr_dup("version", accepted_versions_.begin()->c_str());
812 
813  simple_wml::node& reject = response.root().add_child("reject");
814  reject.set_attr_dup("accepted_versions", utils::join(accepted_versions_).c_str());
815  async_send_doc_queued(socket, response);
816  return;
817  }
818  } else {
819  LOG_SERVER << log_address(socket) << "\tclient didn't send its version: rejecting";
820  return;
821  }
822 
823  std::string username;
824  bool registered, is_moderator;
825 
826  while(true) {
827  auto login_response { coro_receive_doc(socket, yield) };
828  if(!login_response) return;
829 
830  if(const simple_wml::node* const login = login_response->child("login")) {
831  username = (*login)["username"].to_string();
832 
833  if(is_login_allowed(yield, socket, login, username, registered, is_moderator)) {
834  break;
835  } else continue;
836  }
837 
838  async_send_error(socket, "You must login first.", MP_MUST_LOGIN);
839  }
840 
841  simple_wml::node& player_cfg = games_and_users_list_.root().add_child("user");
842  auto [new_player, inserted] = player_connections_.emplace(
843  socket,
844  username,
845  player_cfg,
846  user_handler_ ? user_handler_->get_forum_id(username) : 0,
847  registered,
848  client_version,
849  client_source,
850  user_handler_ ? user_handler_->db_insert_login(username, client_address(socket), client_version) : 0,
853  is_moderator
854  );
855 
856  assert(inserted && "unexpected duplicate username");
857 
858  simple_wml::document join_lobby_response;
859  join_lobby_response.root().add_child("join_lobby").set_attr("is_moderator", is_moderator ? "yes" : "no");
860  simple_wml::node& join_lobby_node = join_lobby_response.root().child("join_lobby")->set_attr_dup("profile_url_prefix", "https://r.wesnoth.org/u");
861  // add server-side queues info
862  simple_wml::node& queues_node = join_lobby_node.add_child("queues");
863  for(const auto& [id, queue] : queue_info_) {
864  simple_wml::node& queue_node = queues_node.add_child("queue");
865  queue_node.set_attr_int("id", queue.id);
866  queue_node.set_attr_dup("scenario_id", queue.scenario_id.c_str());
867  queue_node.set_attr_dup("display_name", queue.display_name.c_str());
868  queue_node.set_attr_int("players_required", queue.players_required);
869  queue_node.set_attr_dup("current_players", utils::join(queue.players_in_queue).c_str());
870  }
871  coro_send_doc(socket, join_lobby_response, yield);
872 
873  boost::asio::spawn(io_service_,
874  [this, socket, new_player](boost::asio::yield_context yield) { handle_player(std::move(yield), socket, new_player); }
875 #if BOOST_VERSION >= 108000
876  , [](const std::exception_ptr& e) { if (e) std::rethrow_exception(e); }
877 #endif
878  );
879 
880  LOG_SERVER << log_address(socket) << "\t" << username << "\thas logged on"
881  << (registered ? " to a registered account" : "");
882 
883  std::shared_ptr<game> last_sent;
884  for(const auto& record : player_connections_.get<game_t>()) {
885  auto g_ptr = record.get_game();
886  if(g_ptr != last_sent) {
887  // Note: This string is parsed by the client to identify lobby join messages!
888  g_ptr->send_server_message_to_all(username + " has logged into the lobby");
889  last_sent = g_ptr;
890  }
891  }
892 
893  // Log the IP
894  if(!user_handler_) {
895  connection_log ip_name { username, client_address(socket), {} };
896 
897  if(std::find(ip_log_.begin(), ip_log_.end(), ip_name) == ip_log_.end()) {
898  ip_log_.push_back(ip_name);
899 
900  // Remove the oldest entry if the size of the IP log exceeds the maximum size
901  if(ip_log_.size() > max_ip_log_size_) {
902  ip_log_.pop_front();
903  }
904  }
905  }
906 }
907 
908 template<class SocketPtr> bool server::is_login_allowed(boost::asio::yield_context yield, SocketPtr socket, const simple_wml::node* const login, const std::string& username, bool& registered, bool& is_moderator)
909 {
910  // Check if the username is valid (all alpha-numeric plus underscore and hyphen)
911  if(!utils::isvalid_username(username)) {
912  async_send_error(socket,
913  "The nickname '" + username + "' contains invalid "
914  "characters. Only alpha-numeric characters, underscores and hyphens are allowed.",
916  );
917 
918  return false;
919  }
920 
921  if(username.size() > 20) {
922  async_send_error(socket, "The nickname '" + username + "' is too long. Nicks must be 20 characters or less.",
924 
925  return false;
926  }
927 
928  // Check if the username is allowed.
929  for(const std::string& d : disallowed_names_) {
931  async_send_error(socket, "The nickname '" + username + "' is reserved and cannot be used by players",
933 
934  return false;
935  }
936  }
937 
938  // Check the username isn't already taken
939  auto p = player_connections_.get<name_t>().find(username);
940  bool name_taken = p != player_connections_.get<name_t>().end();
941 
942  // Check for password
943 
944  if(!authenticate(socket, username, (*login)["password"].to_string(), name_taken, registered))
945  return false;
946 
947  // If we disallow unregistered users and this user is not registered send an error
948  if(user_handler_ && !registered && deny_unregistered_login_) {
949  async_send_error(socket,
950  "The nickname '" + username + "' is not registered. This server disallows unregistered nicknames.",
952  );
953 
954  return false;
955  }
956 
957  is_moderator = user_handler_ && user_handler_->user_is_moderator(username);
958  user_handler::ban_info auth_ban;
959 
960  if(user_handler_) {
961  auth_ban = user_handler_->user_is_banned(username, client_address(socket));
962  }
963 
964  if(auth_ban.type) {
965  std::string ban_type_desc;
966  std::string ban_reason;
967  const char* msg_numeric;
968  std::string ban_duration = std::to_string(auth_ban.duration.count());
969 
970  switch(auth_ban.type) {
972  ban_type_desc = "account";
973  msg_numeric = MP_NAME_AUTH_BAN_USER_ERROR;
974  ban_reason = "a ban has been issued on your user account.";
975  break;
977  ban_type_desc = "IP address";
978  msg_numeric = MP_NAME_AUTH_BAN_IP_ERROR;
979  ban_reason = "a ban has been issued on your IP address.";
980  break;
982  ban_type_desc = "email address";
983  msg_numeric = MP_NAME_AUTH_BAN_EMAIL_ERROR;
984  ban_reason = "a ban has been issued on your email address.";
985  break;
986  default:
987  ban_type_desc = "<unknown ban type>";
988  msg_numeric = "";
989  ban_reason = ban_type_desc;
990  }
991 
992  ban_reason += " (" + ban_duration + ")";
993 
994  if(!is_moderator) {
995  LOG_SERVER << log_address(socket) << "\t" << username << "\tis banned by user_handler (" << ban_type_desc
996  << ")";
997  if(auth_ban.duration > 0s) {
998  // Temporary ban
999  async_send_error(socket, "You are banned from this server: " + ban_reason, msg_numeric, {{"duration", ban_duration}});
1000  } else {
1001  // Permanent ban
1002  async_send_error(socket, "You are banned from this server: " + ban_reason, msg_numeric);
1003  }
1004  return false;
1005  } else {
1006  LOG_SERVER << log_address(socket) << "\t" << username << "\tis banned by user_handler (" << ban_type_desc
1007  << "), " << "ignoring due to moderator flag";
1008  }
1009  }
1010 
1011  if(name_taken) {
1012  if(registered) {
1013  // If there is already a client using this username kick it
1014  process_command("kick " + username + " autokick by registered user", username);
1015  // need to wait for it to process
1016  while(player_connections_.get<name_t>().count(username) > 0) {
1017  boost::asio::post(yield);
1018  }
1019  } else {
1020  async_send_error(socket, "The nickname '" + username + "' is already taken.", MP_NAME_TAKEN_ERROR);
1021  return false;
1022  }
1023  }
1024 
1025  if(auth_ban.type) {
1026  send_server_message(socket, "You are currently banned by the forum administration.", "alert");
1027  }
1028 
1029  return true;
1030 }
1031 
1032 template<class SocketPtr> bool server::authenticate(
1033  SocketPtr socket, const std::string& username, const std::string& password, bool name_taken, bool& registered)
1034 {
1035  // Current login procedure for registered nicks is:
1036  // - Client asks to log in with a particular nick
1037  // - Server sends client a password request (if TLS/database support is enabled)
1038  // - Client sends the plaintext password
1039  // - Server receives plaintext password, hashes it, and compares it to the password in the forum database
1040 
1041  registered = false;
1042 
1043  if(user_handler_) {
1044  const bool exists = user_handler_->user_exists(username);
1045 
1046  // This name is registered but the account is not active
1047  if(exists && !user_handler_->user_is_active(username)) {
1048  async_send_warning(socket,
1049  "The nickname '" + username + "' is inactive. You cannot claim ownership of this "
1050  "nickname until you activate your account via email or ask an administrator to do it for you.",
1052  } else if(exists) {
1053  const std::string salt = user_handler_->extract_salt(username);
1054  if(salt.empty()) {
1055  async_send_error(socket,
1056  "Even though your nickname is registered on this server you "
1057  "cannot log in due to an error in the hashing algorithm. "
1058  "Logging into your forum account on https://forums.wesnoth.org "
1059  "may fix this problem.");
1060  return false;
1061  }
1062  const std::string hashed_password = hash_password(password, salt, username);
1063 
1064  // This name is registered and no password provided
1065  if(password.empty()) {
1066  if(!name_taken) {
1067  send_password_request(socket, "The nickname '" + username + "' is registered on this server.", MP_PASSWORD_REQUEST);
1068  } else {
1069  send_password_request(socket,
1070  "The nickname '" + username + "' is registered on this server."
1071  "\n\nWARNING: There is already a client using this username, "
1072  "logging in will cause that client to be kicked!",
1074  );
1075  }
1076 
1077  return false;
1078  }
1079 
1080  // hashing the password failed
1081  // note: this could be due to other related problems other than *just* the hashing step failing
1082  if(hashed_password.empty()) {
1083  async_send_error(socket, "Password hashing failed.", MP_HASHING_PASSWORD_FAILED);
1084  return false;
1085  }
1086  // This name is registered and an incorrect password provided
1087  else if(!(user_handler_->login(username, hashed_password))) {
1088  const auto steady_now = std::chrono::steady_clock::now();
1089 
1090  login_log login_ip { client_address(socket), 0, steady_now };
1091  auto i = std::find(failed_logins_.begin(), failed_logins_.end(), login_ip);
1092 
1093  if(i == failed_logins_.end()) {
1094  failed_logins_.push_back(login_ip);
1095  i = --failed_logins_.end();
1096 
1097  // Remove oldest entry if maximum size is exceeded
1099  failed_logins_.pop_front();
1100  }
1101  }
1102 
1103  if(i->first_attempt + failed_login_ban_ < steady_now) {
1104  // Clear and move to the beginning
1105  failed_logins_.erase(i);
1106  failed_logins_.push_back(login_ip);
1107  i = --failed_logins_.end();
1108  }
1109 
1110  i->attempts++;
1111 
1112  if(i->attempts > failed_login_limit_) {
1113  LOG_SERVER << ban_manager_.ban(login_ip.ip, std::chrono::system_clock::now() + failed_login_ban_,
1114  "Maximum login attempts exceeded", "automatic", "", username);
1115 
1116  async_send_error(socket, "You have made too many failed login attempts.", MP_TOO_MANY_ATTEMPTS_ERROR);
1117  } else {
1118  send_password_request(socket,
1119  "The password you provided for the nickname '" + username + "' was incorrect.",
1121  }
1122 
1123  // Log the failure
1124  LOG_SERVER << log_address(socket) << "\t"
1125  << "Login attempt with incorrect password for nickname '" << username << "'.";
1126  return false;
1127  }
1128 
1129  // This name exists and the password was neither empty nor incorrect
1130  registered = true;
1131  user_handler_->user_logged_in(username);
1132  }
1133  }
1134 
1135  return true;
1136 }
1137 
1138 template<class SocketPtr> void server::send_password_request(SocketPtr socket,
1139  const std::string& msg,
1140  const char* error_code,
1141  bool force_confirmation)
1142 {
1144  simple_wml::node& e = doc.root().add_child("error");
1145  e.set_attr_dup("message", msg.c_str());
1146  e.set_attr("password_request", "yes");
1147  e.set_attr("force_confirmation", force_confirmation ? "yes" : "no");
1148 
1149  if(*error_code != '\0') {
1150  e.set_attr("error_code", error_code);
1151  }
1152 
1153  async_send_doc_queued(socket, doc);
1154 }
1155 
1156 template<class SocketPtr> void server::handle_player(boost::asio::yield_context yield, SocketPtr socket, player_iterator player)
1157 {
1158  if(lan_server_ > 0s)
1160 
1161  BOOST_SCOPE_EXIT_ALL(this, &player) {
1162  if(!destructed) {
1164  }
1165  };
1166 
1168 
1169  if(!motd_.empty()) {
1171  }
1172  send_server_message(player, information_, "server_info");
1174  if(version_info(player->info().version()) < secure_version ){
1175  send_server_message(player, "You are using version " + player->info().version() + " which has known security issues that can be used to compromise your computer. We strongly recommend updating to a Wesnoth version " + secure_version.str() + " or newer!", "alert");
1176  }
1178  send_server_message(player, "A newer Wesnoth version, " + recommended_version_ + ", is out!", "alert");
1179  }
1180 
1181  // Send other players in the lobby the update that the player has joined
1182  simple_wml::document diff;
1183  make_add_diff(games_and_users_list_.root(), nullptr, "user", diff);
1184  send_to_lobby(diff, player);
1185 
1186  while(true) {
1187  auto doc { coro_receive_doc(socket, yield) };
1188  if(!doc) return;
1189 
1190  // DBG_SERVER << client_address(socket) << "\tWML received:\n" << doc->output();
1191  if(doc->child("refresh_lobby")) {
1193  continue;
1194  }
1195 
1196  if(simple_wml::node* whisper = doc->child("whisper")) {
1197  handle_whisper(player, *whisper);
1198  continue;
1199  }
1200 
1201  if(simple_wml::node* query = doc->child("query")) {
1202  handle_query(player, *query);
1203  continue;
1204  }
1205 
1206  if(simple_wml::node* nickserv = doc->child("nickserv")) {
1207  handle_nickserv(player, *nickserv);
1208  continue;
1209  }
1210 
1211  if(!player_is_in_game(player)) {
1213  } else {
1215  }
1216  }
1217 }
1218 
1220 {
1221  if(simple_wml::node* message = data.child("message")) {
1222  handle_message(player, *message);
1223  return;
1224  }
1225 
1226  if(simple_wml::node* create_game = data.child("create_game")) {
1227  handle_create_game(player, *create_game);
1228  return;
1229  }
1230 
1231  if(simple_wml::node* join = data.child("join")) {
1233  return;
1234  }
1235 
1236  if(simple_wml::node* join_server_queue = data.child("join_server_queue")) {
1237  handle_join_server_queue(player, *join_server_queue);
1238  return;
1239  }
1240 
1241  if(simple_wml::node* leave_server_queue = data.child("leave_server_queue")) {
1242  handle_leave_server_queue(player, *leave_server_queue);
1243  return;
1244  }
1245 
1246  if(simple_wml::node* request = data.child("game_history_request")) {
1247  if(user_handler_) {
1248  int offset = request->attr("offset").to_int();
1249  int player_id = 0;
1250 
1251  // if search_for attribute for offline player -> query the forum database for the forum id
1252  // if search_for attribute for online player -> get the forum id from wesnothd's player info
1253  if(request->has_attr("search_player") && request->attr("search_player").to_string() != "") {
1254  std::string player_name = request->attr("search_player").to_string();
1255  auto player_ptr = player_connections_.get<name_t>().find(player_name);
1256  if(player_ptr == player_connections_.get<name_t>().end()) {
1257  player_id = user_handler_->get_forum_id(player_name);
1258  } else {
1259  player_id = player_ptr->info().config_address()->attr("forum_id").to_int();
1260  }
1261  }
1262 
1263  std::string search_game_name = request->attr("search_game_name").to_string();
1264  int search_content_type = request->attr("search_content_type").to_int();
1265  std::string search_content = request->attr("search_content").to_string();
1266  LOG_SERVER << "Querying game history requested by player `" << player->info().name() << "` for player id `" << player_id << "`."
1267  << "Searching for game name `" << search_game_name << "`, search content type `" << search_content_type << "`, search content `" << search_content << "`.";
1268  user_handler_->async_get_and_send_game_history(io_service_, *this, player->socket(), player_id, offset, search_game_name, search_content_type, search_content);
1269  }
1270  return;
1271  }
1272 }
1273 
1275 {
1276  if((whisper["receiver"].empty()) || (whisper["message"].empty())) {
1277  static simple_wml::document data(
1278  "[message]\n"
1279  "message=\"Invalid number of arguments\"\n"
1280  "sender=\"server\"\n"
1281  "[/message]\n",
1283  );
1284 
1286  return;
1287  }
1288 
1289  whisper.set_attr_dup("sender", player->name().c_str());
1290 
1291  auto receiver_iter = player_connections_.get<name_t>().find(whisper["receiver"].to_string());
1292  if(receiver_iter == player_connections_.get<name_t>().end()) {
1293  send_server_message(player, "Can't find '" + whisper["receiver"].to_string() + "'.", "error");
1294  return;
1295  }
1296 
1297  auto g = player->get_game();
1298  if(g && g->started() && g->is_player(player_connections_.project<0>(receiver_iter))) {
1299  send_server_message(player, "You cannot send private messages to players in a running game you observe.", "error");
1300  return;
1301  }
1302 
1303  simple_wml::document cwhisper;
1304 
1305  simple_wml::node& trunc_whisper = cwhisper.root().add_child("whisper");
1306  whisper.copy_into(trunc_whisper);
1307 
1308  const simple_wml::string_span& msg = trunc_whisper["message"];
1309  chat_message::truncate_message(msg, trunc_whisper);
1310 
1311  send_to_player(player_connections_.project<0>(receiver_iter), cwhisper);
1312 }
1313 
1315 {
1316  wesnothd::player& player = iter->info();
1317 
1318  const std::string command(query["type"].to_string());
1319  std::ostringstream response;
1320 
1321  const std::string& query_help_msg =
1322  "Available commands are: adminmsg <msg>, help, games, metrics,"
1323  " motd, requests, roll <sides>, sample, stats, status, version, wml.";
1324 
1325  // Commands a player may issue.
1326  if(command == "status") {
1327  response << process_command(command + " " + player.name(), player.name());
1328  } else if(
1329  command.compare(0, 8, "adminmsg") == 0 ||
1330  command.compare(0, 6, "report") == 0 ||
1331  command == "games" ||
1332  command == "metrics" ||
1333  command == "motd" ||
1334  command.compare(0, 7, "version") == 0 ||
1335  command == "requests" ||
1336  command.compare(0, 4, "roll") == 0 ||
1337  command == "sample" ||
1338  command == "stats" ||
1339  command == "status " + player.name() ||
1340  command == "wml"
1341  ) {
1342  response << process_command(command, player.name());
1343  } else if(player.is_moderator()) {
1344  if(command == "signout") {
1345  LOG_SERVER << "Admin signed out: IP: " << iter->client_ip() << "\tnick: " << player.name();
1346  player.set_moderator(false);
1347  // This string is parsed by the client!
1348  response << "You are no longer recognized as an administrator.";
1349  if(user_handler_) {
1350  user_handler_->set_is_moderator(player.name(), false);
1351  }
1352  } else {
1353  LOG_SERVER << "Admin Command: type: " << command << "\tIP: " << iter->client_ip()
1354  << "\tnick: " << player.name();
1355  response << process_command(command, player.name());
1356  LOG_SERVER << response.str();
1357  }
1358  } else if(command == "help" || command.empty()) {
1359  response << query_help_msg;
1360  } else if(command == "admin" || command.compare(0, 6, "admin ") == 0) {
1361  if(admin_passwd_.empty()) {
1362  send_server_message(iter, "No password set.", "error");
1363  return;
1364  }
1365 
1366  std::string passwd;
1367  if(command.size() >= 6) {
1368  passwd = command.substr(6);
1369  }
1370 
1371  if(passwd == admin_passwd_) {
1372  LOG_SERVER << "New Admin recognized: IP: " << iter->client_ip() << "\tnick: " << player.name();
1373  player.set_moderator(true);
1374  // This string is parsed by the client!
1375  response << "You are now recognized as an administrator.";
1376 
1377  if(user_handler_) {
1378  user_handler_->set_is_moderator(player.name(), true);
1379  }
1380  } else {
1381  WRN_SERVER << "FAILED Admin attempt with password: '" << passwd << "'\tIP: " << iter->client_ip()
1382  << "\tnick: " << player.name();
1383  response << "Error: wrong password";
1384  }
1385  } else {
1386  response << "Error: unrecognized query: '" << command << "'\n" << query_help_msg;
1387  }
1388 
1389  send_server_message(iter, response.str(), "info");
1390 }
1391 
1393 {
1394  // Check if this server allows nick registration at all
1395  if(!user_handler_) {
1396  send_server_message(player, "This server does not allow username registration.", "error");
1397  return;
1398  }
1399 
1400  // A user requested a list of which details can be set
1401  if(nickserv.child("info")) {
1402  try {
1403  std::string res = user_handler_->user_info((*nickserv.child("info"))["name"].to_string());
1404  send_server_message(player, res, "info");
1405  } catch(const user_handler::error& e) {
1407  "There was an error looking up the details of the user '"
1408  + (*nickserv.child("info"))["name"].to_string() + "'. "
1409  + " The error message was: " + e.message, "error"
1410  );
1411  }
1412 
1413  return;
1414  }
1415 }
1416 
1418 {
1419  if(user->info().is_message_flooding()) {
1420  send_server_message(user,
1421  "Warning: you are sending too many messages too fast. Your message has not been relayed.", "error");
1422  return;
1423  }
1424 
1425  simple_wml::document relay_message;
1426  message.set_attr_dup("sender", user->name().c_str());
1427 
1428  simple_wml::node& trunc_message = relay_message.root().add_child("message");
1429  message.copy_into(trunc_message);
1430 
1431  const simple_wml::string_span& msg = trunc_message["message"];
1432  chat_message::truncate_message(msg, trunc_message);
1433 
1434  if(msg.size() >= 3 && simple_wml::string_span(msg.begin(), 4) == "/me ") {
1435  LOG_SERVER << user->client_ip() << "\t<" << user->name()
1436  << simple_wml::string_span(msg.begin() + 3, msg.size() - 3) << ">";
1437  } else {
1438  LOG_SERVER << user->client_ip() << "\t<" << user->name() << "> " << msg;
1439  }
1440 
1441  send_to_lobby(relay_message, user);
1442 }
1443 
1444 void server::send_queue_update(const queue_info& queue, utils::optional<player_iterator> exclude)
1445 {
1446  simple_wml::document queue_update;
1447  simple_wml::node& update = queue_update.root().add_child("queue_update");
1448  update.set_attr_int("queue_id", queue.id);
1449  update.set_attr_dup("action", "update");
1450  update.set_attr_dup("current_players", utils::join(queue.players_in_queue).c_str());
1451 
1452  send_to_lobby(queue_update, exclude);
1453 }
1454 
1456 {
1457  if(graceful_restart) {
1458  static simple_wml::document leave_game_doc("[leave_game]\n[/leave_game]\n", simple_wml::INIT_COMPRESSED);
1459  send_to_player(player, leave_game_doc);
1460 
1462  "This server is shutting down. You aren't allowed to make new games. Please "
1463  "reconnect to the new server.", "error");
1464 
1466  return;
1467  }
1468 
1469  const std::string game_name = create_game["name"].to_string();
1470  const std::string game_password = create_game["password"].to_string();
1471  const std::string initial_bans = create_game["ignored"].to_string();
1472  const queue_type::type queue_type = queue_type::get_enum(create_game["queue_type"].to_string()).value_or(queue_type::type::normal);
1473  int queue_id = create_game["queue_id"].to_int();
1474 
1475  DBG_SERVER << player->client_ip() << "\t" << player->info().name()
1476  << "\tcreates a new game: \"" << game_name << "\".";
1477 
1478  // Create the new game, remove the player from the lobby
1479  // and set the player as the host/owner.
1480  player_connections_.modify(player, [this, player, &game_name, queue_type, queue_id](player_record& host_record) {
1481  host_record.get_game().reset(
1483  std::bind(&server::cleanup_game, this, std::placeholders::_1)
1484  );
1485  });
1486 
1487  wesnothd::game& g = *player->get_game();
1488 
1489  DBG_SERVER << "initial bans: " << initial_bans;
1490  if(initial_bans != "") {
1491  g.set_name_bans(utils::split(initial_bans,','));
1492  }
1493 
1494  if(game_password.empty() == false) {
1495  g.set_password(game_password);
1496  }
1497 
1498  create_game.copy_into(g.level().root());
1499 
1500  // remove from any queues they may have joined
1501  for(int q : player->info().get_queues()) {
1502  queue_info& queue = queue_info_.at(q);
1503  if(!queue.players_in_queue.empty()) {
1504  queue.players_in_queue.erase(std::remove(queue.players_in_queue.begin(), queue.players_in_queue.end(), player->info().name()));
1505  send_queue_update(queue);
1506  }
1507  }
1508 }
1509 
1511 {
1513 
1514  if(user_handler_){
1515  user_handler_->db_update_game_end(uuid_, game_ptr->db_id(), game_ptr->get_replay_filename());
1516  }
1517 
1518  simple_wml::node* const gamelist = games_and_users_list_.child("gamelist");
1519  assert(gamelist != nullptr);
1520 
1521  // Send a diff of the gamelist with the game deleted to players in the lobby
1522  simple_wml::document diff;
1523  if(!destructed && make_delete_diff(*gamelist, "gamelist", "game", game_ptr->description(), diff)) {
1524  send_to_lobby(diff);
1525  }
1526 
1527  // Delete the game from the games_and_users_list_.
1528  const simple_wml::node::child_list& games = gamelist->children("game");
1529  const auto g = std::find(games.begin(), games.end(), game_ptr->description());
1530 
1531  if(g != games.end()) {
1532  const std::size_t index = std::distance(games.begin(), g);
1533  gamelist->remove_child("game", index);
1534  } else {
1535  // Can happen when the game ends before the scenario was transferred.
1536  LOG_SERVER << "Could not find game (" << game_ptr->id() << ", " << game_ptr->db_id() << ") to delete in games_and_users_list_.";
1537  }
1538 
1539  if(destructed) game_ptr->emergency_cleanup();
1540 
1541  delete game_ptr;
1542 }
1543 
1545 {
1546  int game_id = join["id"].to_int();
1547 
1548  // this is a game defined in an [mp_queue] in the client
1549  // if there is no mp_queue defined game already existing with empty slots, tell the client to create one
1550  // else update game_id to the game that already exists and have the client join that game
1551  if(game_id < 0) {
1552  for(const auto& game : games()) {
1553  if(game->q_type() == queue_type::type::client_preset &&
1554  !game->started() &&
1555  join["mp_scenario"].to_string() == game->get_scenario_id() &&
1556  game->description()->child("slot_data")->attr("vacant").to_int() != 0) {
1557  game_id = game->id();
1558  }
1559  }
1560 
1561  // if it's still negative, then there's no existing game to join
1562  if(game_id < 0) {
1563  simple_wml::document create_game_doc;
1564  simple_wml::node& create_game_node = create_game_doc.root().add_child("create_game");
1565  create_game_node.set_attr_dup("mp_scenario", join["mp_scenario"].to_string().c_str());
1566  create_game_node.set_attr_int("is_preset", 1);
1567 
1568  // tell the client to create a game since there is no suitable existing game to join
1569  send_to_player(player, create_game_doc);
1570  return;
1571  } else {
1572  simple_wml::document join_game_doc;
1573  simple_wml::node& join_game_node = join_game_doc.root().add_child("join_game");
1574  join_game_node.set_attr_int("id", game_id);
1575 
1576  // tell the client to create a game since there is no suitable existing game to join
1577  send_to_player(player, join_game_doc);
1578  return;
1579  }
1580  }
1581 
1582  const bool observer = join.attr("observe").to_bool();
1583  const std::string& password = join["password"].to_string();
1584 
1585  auto g_iter = player_connections_.get<game_t>().find(game_id);
1586 
1587  std::shared_ptr<game> g;
1588  if(g_iter != player_connections_.get<game_t>().end()) {
1589  g = g_iter->get_game();
1590  }
1591 
1592  static simple_wml::document leave_game_doc("[leave_game]\n[/leave_game]\n", simple_wml::INIT_COMPRESSED);
1593  if(!g) {
1594  WRN_SERVER << player->client_ip() << "\t" << player->info().name()
1595  << "\tattempted to join unknown game:\t" << game_id << ".";
1596  send_to_player(player, leave_game_doc);
1597  send_server_message(player, "Attempt to join unknown game.", "error");
1599  return;
1600  } else if(!g->level_init()) {
1601  WRN_SERVER << player->client_ip() << "\t" << player->info().name()
1602  << "\tattempted to join uninitialized game:\t\"" << g->name() << "\" (" << game_id << ").";
1603  send_to_player(player, leave_game_doc);
1604  send_server_message(player, "Attempt to join an uninitialized game.", "error");
1606  return;
1607  } else if(player->info().is_moderator()) {
1608  // Admins are always allowed to join.
1609  } else if(g->player_is_banned(player, player->info().name())) {
1610  DBG_SERVER << player->client_ip()
1611  << "\tReject banned player: " << player->info().name()
1612  << "\tfrom game:\t\"" << g->name() << "\" (" << game_id << ").";
1613  send_to_player(player, leave_game_doc);
1614  send_server_message(player, "You are banned from this game.", "error");
1616  return;
1617  } else if(!g->password_matches(password)) {
1618  WRN_SERVER << player->client_ip() << "\t" << player->info().name()
1619  << "\tattempted to join game:\t\"" << g->name() << "\" (" << game_id << ") with bad password";
1620  send_to_player(player, leave_game_doc);
1621  send_server_message(player, "Incorrect password.", "error");
1623  return;
1624  }
1625 
1626  bool joined = g->add_player(player, observer);
1627  if(!joined) {
1628  WRN_SERVER << player->client_ip() << "\t" << player->info().name()
1629  << "\tattempted to observe game:\t\"" << g->name() << "\" (" << game_id
1630  << ") which doesn't allow observers.";
1631  send_to_player(player, leave_game_doc);
1632 
1634  "Attempt to observe a game that doesn't allow observers. (You probably joined the "
1635  "game shortly after it filled up.)", "error");
1636 
1638  return;
1639  }
1640 
1641  player_connections_.modify(player,
1642  std::bind(&player_record::set_game, std::placeholders::_1, g));
1643 
1644  g->describe_slots();
1645 
1646  // send notification of changes to the game and user
1647  simple_wml::document diff;
1648  bool diff1 = make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", g->changed_description(), diff);
1649  bool diff2 = make_change_diff(games_and_users_list_.root(), nullptr, "user", player->info().config_address(), diff);
1650 
1651  if(diff1 || diff2) {
1652  send_to_lobby(diff);
1653  }
1654 
1655  // remove from any queues they may have joined
1656  for(int q : player->info().get_queues()) {
1657  queue_info& queue = queue_info_.at(q);
1658  if(!queue.players_in_queue.empty()) {
1659  queue.players_in_queue.erase(std::remove(queue.players_in_queue.begin(), queue.players_in_queue.end(), player->info().name()));
1660  send_queue_update(queue);
1661  }
1662  }
1663 }
1664 
1666 {
1667  int queue_id = data.attr("queue_id").to_int();
1668 
1669  if(queue_info_.count(queue_id) == 0) {
1670  ERR_SERVER << "player " << p->info().name() << " attempted to join non-existing server-side queue " << data.attr("queue_id");
1671  return;
1672  }
1673 
1674  queue_info& queue = queue_info_.at(queue_id);
1675  if(utils::contains(queue.players_in_queue, p->info().name())) {
1676  DBG_SERVER << "player " << p->info().name() << " already in server-side queue " << data.attr("queue_id");
1677  return;
1678  }
1679 
1680  // if they're not already in the queue, add them
1681  queue.players_in_queue.emplace_back(p->info().name());
1682  p->info().add_queue(queue.id);
1683  LOG_SERVER << p->client_ip() << "\t" << p->name() << "\tincrementing " << queue.scenario_id << " to " << std::to_string(queue.players_in_queue.size()) << "/" << std::to_string(queue.players_required);
1684 
1685  send_queue_update(queue);
1686 
1687  // if there are enough players in the queue to start a game, then have the final player who joined the queue host it
1688  // else check if there's an existing game that was created for the queue which needs players (ie: player left or failed to join)
1689  // if yes, tell the player to immediately join that game
1690  // if no, leave them in the queue
1691  if(queue.players_required <= queue.players_in_queue.size()) {
1692  simple_wml::document create_game_doc;
1693  simple_wml::node& create_game_node = create_game_doc.root().add_child("create_game");
1694  create_game_node.set_attr_int("queue_id", queue.id);
1695  simple_wml::node& game = create_game_node.add_child("game");
1696  game.set_attr_dup("scenario", queue.settings["scenario"].str().c_str());
1697  game.set_attr_dup("era", queue.settings["era"].str().c_str());
1698  game.set_attr_int("fog", queue.settings["fog"].to_int());
1699  game.set_attr_int("shroud", queue.settings["shroud"].to_int());
1700  game.set_attr_int("village_gold", queue.settings["village_gold"].to_int());
1701  game.set_attr_int("village_support", queue.settings["village_support"].to_int());
1702  game.set_attr_int("experience_modifier", queue.settings["experience_modifier"].to_int());
1703  game.set_attr_int("countdown", queue.settings["countdown"].to_int());
1704  game.set_attr_int("random_start_time", queue.settings["random_start_time"].to_int());
1705  game.set_attr_int("shuffle_sides", queue.settings["shuffle_sides"].to_int());
1706 
1707  // tell the final player to create and host the game
1708  send_to_player(p, create_game_doc);
1709  } else {
1710  for(const auto& game : games()) {
1711  if(game->is_open_queue_game(queue.id)) {
1712  simple_wml::document join_game_doc;
1713  simple_wml::node& join_game_node = join_game_doc.root().add_child("join_game");
1714  join_game_node.set_attr_int("id", game->id());
1715 
1716  send_to_player(p, join_game_doc);
1717  return;
1718  }
1719  }
1720  }
1721 }
1722 
1724 {
1725  int queue_id = data.attr("queue_id").to_int();
1726 
1727  if(queue_info_.count(queue_id) == 0) {
1728  ERR_SERVER << "player " << p->info().name() << " attempted to leave non-existing server-side queue " << data.attr("queue_id");
1729  return;
1730  }
1731 
1732  queue_info& queue = queue_info_.at(queue_id);
1733 
1734  // if they're in the queue, remove them
1735  if(utils::contains(queue.players_in_queue, p->info().name())) {
1736  queue.players_in_queue.erase(std::remove(queue.players_in_queue.begin(), queue.players_in_queue.end(), p->info().name()));
1737  p->info().clear_queues();
1738 
1739  send_queue_update(queue);
1740  } else {
1741  ERR_SERVER << "player " << p->info().name() << " already not in server-side queue " << data.attr("queue_id");
1742  }
1743 }
1744 
1746 {
1747  DBG_SERVER << "in process_data_game...";
1748 
1749  wesnothd::player& player { p->info() };
1750 
1751  game& g = *(p->get_game());
1752  std::weak_ptr<game> g_ptr{p->get_game()};
1753 
1754  // If this is data describing the level for a game.
1755  if(data.child("snapshot") || data.child("scenario")) {
1756  if(!g.is_owner(p)) {
1757  return;
1758  }
1759 
1760  // If this game is having its level data initialized
1761  // for the first time, and is ready for players to join.
1762  // We should currently have a summary of the game in g.level().
1763  // We want to move this summary to the games_and_users_list_, and
1764  // place a pointer to that summary in the game's description.
1765  // g.level() should then receive the full data for the game.
1766  if(!g.level_init()) {
1767  LOG_SERVER << p->client_ip() << "\t" << player.name() << "\tcreated game:\t\"" << g.name() << "\" ("
1768  << g.id() << ", " << g.db_id() << ").";
1769  // Update our config object which describes the open games,
1770  // and save a pointer to the description in the new game.
1771  simple_wml::node* const gamelist = games_and_users_list_.child("gamelist");
1772  assert(gamelist != nullptr);
1773 
1774  simple_wml::node& desc = gamelist->add_child("game");
1775  g.level().root().copy_into(desc);
1776 
1777  if(const simple_wml::node* m = data.child("multiplayer")) {
1778  m->copy_into(desc);
1779  } else {
1780  WRN_SERVER << p->client_ip() << "\t" << player.name() << "\tsent scenario data in game:\t\""
1781  << g.name() << "\" (" << g.id() << ", " << g.db_id() << ") without a 'multiplayer' child.";
1782  // Set the description so it can be removed in delete_game().
1783  g.set_description(&desc);
1784  delete_game(g.id());
1785 
1787  "The scenario data is missing the [multiplayer] tag which contains the "
1788  "game settings. Game aborted.", "error");
1789  return;
1790  }
1791 
1792  g.set_description(&desc);
1793  desc.set_attr_dup("id", std::to_string(g.id()).c_str());
1794  } else {
1795  WRN_SERVER << p->client_ip() << "\t" << player.name() << "\tsent scenario data in game:\t\""
1796  << g.name() << "\" (" << g.id() << ", " << g.db_id() << ") although it's already initialized.";
1797  return;
1798  }
1799 
1800  assert(games_and_users_list_.child("gamelist")->children("game").empty() == false);
1801 
1802  simple_wml::node& desc = *g.description_for_writing();
1803 
1804  // Update the game's description.
1805  // If there is no shroud, then tell players in the lobby
1806  // what the map looks like
1808  // fixme: the hanlder of [store_next_scenario] below searches for 'mp_shroud' in [scenario]
1809  // at least of the these cosed is likely wrong.
1810  if(!data["mp_shroud"].to_bool()) {
1811  desc.set_attr_dup("map_data", s["map_data"]);
1812  }
1813 
1814  if(const simple_wml::node* e = data.child("era")) {
1815  if(!e->attr("require_era").to_bool(true)) {
1816  desc.set_attr("require_era", "no");
1817  }
1818  }
1819 
1820  if(s["require_scenario"].to_bool(false)) {
1821  desc.set_attr("require_scenario", "yes");
1822  }
1823 
1824  const simple_wml::node::child_list& mlist = data.children("modification");
1825  for(const simple_wml::node* m : mlist) {
1826  desc.add_child_at("modification", 0);
1827  desc.child("modification")->set_attr_dup("id", m->attr("id"));
1828  desc.child("modification")->set_attr_dup("name", m->attr("name"));
1829  desc.child("modification")->set_attr_dup("addon_id", m->attr("addon_id"));
1830  desc.child("modification")->set_attr_dup("require_modification", m->attr("require_modification"));
1831  }
1832 
1833  // Record the full scenario in g.level()
1834  g.level().swap(data);
1835 
1836  // The host already put himself in the scenario so we just need
1837  // to update_side_data().
1838  // g.take_side(sock);
1839  g.update_side_data();
1840  g.describe_slots();
1841 
1842  // Send the update of the game description to the lobby.
1843  simple_wml::document diff;
1844  make_add_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", diff);
1845  make_change_diff(games_and_users_list_.root(), nullptr, "user", p->info().config_address(), diff);
1846 
1847  send_to_lobby(diff);
1848 
1849  // if this is the creation of a game from a server-side queue, need to tell all the other players in the queue to join
1850  if(g.q_type() == queue_type::type::server_preset) {
1851  int queue_id = g.queue_id();
1852  int game_id = g.id();
1853 
1854  if(queue_info_.count(queue_id) == 0) {
1855  return;
1856  }
1857 
1858  queue_info& info = queue_info_.at(queue_id);
1859  std::size_t joined_count = 1;
1860  for(const std::string& name : info.players_in_queue) {
1861  auto player_ptr = player_connections_.get<name_t>().find(name);
1862 
1863  // player is still connected and not in a game, tell them to join
1864  if(player_ptr != player_connections_.get<name_t>().end() && !player_ptr->get_game()) {
1865  simple_wml::document join_game_doc;
1866  simple_wml::node& join_game_node = join_game_doc.root().add_child("join_game");
1867  join_game_node.set_attr_int("id", game_id);
1868  send_to_player(player_ptr->socket(), join_game_doc);
1869  }
1870 
1871  // remove them from any queues they joined
1872  for(int queue : player_ptr->info().get_queues()) {
1873  queue_info& other_queue = queue_info_.at(queue);
1874  if(!other_queue.players_in_queue.empty()) {
1875  other_queue.players_in_queue.erase(std::remove(other_queue.players_in_queue.begin(), other_queue.players_in_queue.end(), player_ptr->info().name()));
1876  }
1877  }
1878 
1879  joined_count++;
1880  if(joined_count == info.players_required) {
1881  break;
1882  }
1883  }
1884 
1885  // send all other players the updated player counts for all queues
1886  for(auto& [id, queue] : queue_info_) {
1887  send_queue_update(queue);
1888  }
1889  }
1890 
1891  /** @todo FIXME: Why not save the level data in the history_? */
1892  return;
1893  // Everything below should only be processed if the game is already initialized.
1894  } else if(!g.level_init()) {
1895  WRN_SERVER << p->client_ip() << "\tReceived unknown data from: " << player.name()
1896  << " while the scenario wasn't yet initialized."
1897  << data.output();
1898  return;
1899  // If the host is sending the next scenario data.
1900  } else if(const simple_wml::node* scenario = data.child("store_next_scenario")) {
1901  if(!g.is_owner(p)) {
1902  return;
1903  }
1904 
1905  if(!g.level_init()) {
1906  WRN_SERVER << p->client_ip() << "\tWarning: " << player.name()
1907  << "\tsent [store_next_scenario] in game:\t\"" << g.name() << "\" (" << g.id()
1908  << ", " << g.db_id() << ") while the scenario is not yet initialized.";
1909  return;
1910  }
1911 
1912  g.save_replay();
1913  if(user_handler_){
1914  user_handler_->db_update_game_end(uuid_, g.db_id(), g.get_replay_filename());
1915  }
1916 
1917  g.new_scenario(p);
1918  g.reset_last_synced_context_id();
1919 
1920  // Record the full scenario in g.level()
1921  g.level().clear();
1922  scenario->copy_into(g.level().root());
1923  g.next_db_id();
1924 
1925  if(g.description() == nullptr) {
1926  ERR_SERVER << p->client_ip() << "\tERROR: \"" << g.name() << "\" (" << g.id()
1927  << ", " << g.db_id() << ") is initialized but has no description_.";
1928  return;
1929  }
1930 
1931  simple_wml::node& desc = *g.description_for_writing();
1932 
1933  // Update the game's description.
1934  if(const simple_wml::node* m = scenario->child("multiplayer")) {
1935  m->copy_into(desc);
1936  } else {
1937  WRN_SERVER << p->client_ip() << "\t" << player.name() << "\tsent scenario data in game:\t\""
1938  << g.name() << "\" (" << g.id() << ", " << g.db_id() << ") without a 'multiplayer' child.";
1939 
1940  delete_game(g.id());
1941 
1943  "The scenario data is missing the [multiplayer] tag which contains the game "
1944  "settings. Game aborted.", "error");
1945  return;
1946  }
1947 
1948  // If there is no shroud, then tell players in the lobby
1949  // what the map looks like.
1950  const simple_wml::node& s = *wesnothd::game::starting_pos(g.level().root());
1951  desc.set_attr_dup("map_data", s["mp_shroud"].to_bool() ? "" : s["map_data"]);
1952 
1953  if(const simple_wml::node* e = data.child("era")) {
1954  if(!e->attr("require_era").to_bool(true)) {
1955  desc.set_attr("require_era", "no");
1956  }
1957  }
1958 
1959  if(s["require_scenario"].to_bool(false)) {
1960  desc.set_attr("require_scenario", "yes");
1961  }
1962 
1963  // Tell everyone that the next scenario data is available.
1964  static simple_wml::document notify_next_scenario(
1965  "[notify_next_scenario]\n[/notify_next_scenario]\n", simple_wml::INIT_COMPRESSED);
1966  g.send_data(notify_next_scenario, p);
1967 
1968  // Send the update of the game description to the lobby.
1970  return;
1971  // A mp client sends a request for the next scenario of a mp campaign.
1972  } else if(data.child("load_next_scenario")) {
1973  g.load_next_scenario(p);
1974  return;
1975  } else if(data.child("start_game")) {
1976  if(!g.is_owner(p)) {
1977  return;
1978  }
1979 
1980  // perform controller tweaks, assigning sides as human for their owners etc.
1981  g.perform_controller_tweaks();
1982 
1983  // Send notification of the game starting immediately.
1984  // g.start_game() will send data that assumes
1985  // the [start_game] message has been sent
1986  g.send_data(data, p);
1987  g.start_game(p);
1988 
1989  if(user_handler_) {
1990  const simple_wml::node& m = *g.level().root().child("multiplayer");
1992  // [addon] info handling
1993  std::set<std::string> primary_keys;
1994  for(const auto& addon : m.children("addon")) {
1995  for(const auto& content : addon->children("content")) {
1996  std::string key = uuid_+"-"+std::to_string(g.db_id())+"-"+content->attr("type").to_string()+"-"+content->attr("id").to_string()+"-"+addon->attr("id").to_string();
1997  if(primary_keys.count(key) == 0) {
1998  primary_keys.emplace(key);
1999  unsigned long long rows_inserted = user_handler_->db_insert_game_content_info(uuid_, g.db_id(), content->attr("type").to_string(), content->attr("name").to_string(), content->attr("id").to_string(), addon->attr("id").to_string(), addon->attr("version").to_string());
2000  if(rows_inserted == 0) {
2001  WRN_SERVER << "Did not insert content row for [addon] data with uuid '" << uuid_ << "', game ID '" << g.db_id() << "', type '" << content->attr("type").to_string() << "', and content ID '" << content->attr("id").to_string() << "'";
2002  }
2003  }
2004  }
2005  }
2006  if(m.children("addon").size() == 0) {
2007  WRN_SERVER << "Game content info missing for game with uuid '" << uuid_ << "', game ID '" << g.db_id() << "', named '" << g.name() << "'";
2008  }
2009 
2010  user_handler_->db_insert_game_info(uuid_, g.db_id(), server_id_, g.name(), g.is_reload(), m["observer"].to_bool(), !m["private_replay"].to_bool(), g.has_password());
2011 
2012  const simple_wml::node::child_list& sides = g.get_sides_list();
2013  for(unsigned side_index = 0; side_index < sides.size(); ++side_index) {
2014  const simple_wml::node& side = *sides[side_index];
2015  const auto player = player_connections_.get<name_t>().find(side["player_id"].to_string());
2016  std::string version;
2017  std::string source;
2018 
2019  // if "Nobody" is chosen for a side, for example
2020  if(player == player_connections_.get<name_t>().end()){
2021  version = "";
2022  source = "";
2023  } else {
2024  version = player->info().version();
2025  source = player->info().source();
2026 
2027  if(client_sources_.count(source) == 0) {
2028  source = "Default";
2029  }
2030  }
2031 
2032  // approximately determine leader(s) for the side like the client does
2033  // useful generally to know how often leaders are used vs other leaders
2034  // also as an indication for which faction was chosen if a custom recruit list is provided since that results in "Custom" in the faction field
2035  std::vector<std::string> leaders;
2036  // if a type= attribute is specified for the side, add it
2037  if(side.attr("type") != "") {
2038  leaders.emplace_back(side.attr("type").to_string());
2039  }
2040  // add each [unit] in the side that has canrecruit=yes
2041  for(const auto unit : side.children("unit")) {
2042  if(unit->attr("canrecruit") == "yes") {
2043  leaders.emplace_back(unit->attr("type").to_string());
2044  }
2045  }
2046  // add any [leader] specified for the side
2047  for(const auto leader : side.children("leader")) {
2048  leaders.emplace_back(leader->attr("type").to_string());
2049  }
2050 
2051  user_handler_->db_insert_game_player_info(uuid_, g.db_id(), side["player_id"].to_string(), side["side"].to_int(), side["is_host"].to_bool(), side["faction"].to_string(), version, source, side["current_player"].to_string(), utils::join(leaders));
2052  }
2053  }
2054 
2055  // update the game having changed in the lobby
2057  return;
2058  } else if(data.child("leave_game")) {
2059  if(g.remove_player(p)) {
2060  delete_game(g.id());
2061  } else {
2062  bool has_diff = false;
2063  simple_wml::document diff;
2064 
2065  // After this line, the game object may be destroyed. Don't use `g`!
2066  player_connections_.modify(p, std::bind(&player_record::enter_lobby, std::placeholders::_1));
2067 
2068  // Only run this if the game object is still valid
2069  if(auto gStrong = g_ptr.lock()) {
2070  gStrong->describe_slots();
2071  //Don't update the game if it no longer exists.
2072  has_diff |= make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", gStrong->description(), diff);
2073  }
2074 
2075  // Send all other players in the lobby the update to the gamelist.
2076  has_diff |= make_change_diff(games_and_users_list_.root(), nullptr, "user", player.config_address(), diff);
2077 
2078  if(has_diff) {
2079  send_to_lobby(diff, p);
2080  }
2081 
2082  // Send the player who has quit the gamelist.
2084  }
2085 
2086  // send the current queue counts
2087  for(const auto& [id, info] : queue_info_) {
2088  simple_wml::document queue_update;
2089  simple_wml::node& update = queue_update.root().add_child("queue_update");
2090  update.set_attr_int("queue_id", info.id);
2091  update.set_attr_dup("action", "update");
2092  update.set_attr_dup("current_players", utils::join(info.players_in_queue).c_str());
2093 
2094  send_to_player(p, queue_update);
2095  }
2096 
2097  return;
2098  // If this is data describing side changes by the host.
2099  } else if(const simple_wml::node* scenario_diff = data.child("scenario_diff")) {
2100  if(!g.is_owner(p)) {
2101  return;
2102  }
2103 
2104  g.level().root().apply_diff(*scenario_diff);
2105  const simple_wml::node* cfg_change = scenario_diff->child("change_child");
2106 
2107  if(cfg_change) {
2108  g.update_side_data();
2109  }
2110 
2111  g.describe_slots();
2113 
2114  g.send_data(data, p);
2115  return;
2116  // If a player changes his faction.
2117  } else if(data.child("change_faction")) {
2118  g.send_data(data, p);
2119  return;
2120  // If the owner of a side is changing the controller.
2121  } else if(const simple_wml::node* change = data.child("change_controller")) {
2122  g.transfer_side_control(p, *change);
2123  g.describe_slots();
2125 
2126  return;
2127  // If all observers should be muted. (toggles)
2128  } else if(data.child("muteall")) {
2129  if(!g.is_owner(p)) {
2130  g.send_server_message("You cannot mute: not the game host.", p);
2131  return;
2132  }
2133 
2134  g.mute_all_observers();
2135  return;
2136  // If an observer should be muted.
2137  } else if(const simple_wml::node* mute = data.child("mute")) {
2138  g.mute_observer(*mute, p);
2139  return;
2140  // If an observer should be unmuted.
2141  } else if(const simple_wml::node* unmute = data.child("unmute")) {
2142  g.unmute_observer(*unmute, p);
2143  return;
2144  // The owner is kicking/banning someone from the game.
2145  } else if(data.child("kick") || data.child("ban")) {
2146  bool ban = (data.child("ban") != nullptr);
2147  auto user { ban
2148  ? g.ban_user(*data.child("ban"), p)
2149  : g.kick_member(*data.child("kick"), p)};
2150 
2151  if(user) {
2152  player_connections_.modify(*user, std::bind(&player_record::enter_lobby, std::placeholders::_1));
2153  g.describe_slots();
2154 
2155  update_game_in_lobby(g, user);
2156 
2157  // Send all other players in the lobby the update to the gamelist.
2158  simple_wml::document gamelist_diff;
2159  make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", g.description(), gamelist_diff);
2160  make_change_diff(games_and_users_list_.root(), nullptr, "user", (*user)->info().config_address(), gamelist_diff);
2161 
2162  send_to_lobby(gamelist_diff, p);
2163 
2164  // Send the removed user the lobby game list.
2166  }
2167 
2168  return;
2169  } else if(const simple_wml::node* unban = data.child("unban")) {
2170  g.unban_user(*unban, p);
2171  return;
2172  // If info is being provided about the game state.
2173  } else if(const simple_wml::node* info = data.child("info")) {
2174  if(!g.is_player(p)) {
2175  return;
2176  }
2177 
2178  if((*info)["type"] == "termination") {
2179  g.set_termination_reason((*info)["condition"].to_string());
2180  if((*info)["condition"].to_string() == "out of sync") {
2181  g.send_and_record_server_message(player.name() + " reports out of sync errors.");
2182  if(user_handler_){
2183  user_handler_->db_set_oos_flag(uuid_, g.db_id());
2184  }
2185  }
2186  }
2187 
2188  return;
2189  } else if(data.child("turn")) {
2190  // Notify the game of the commands, and if it changes
2191  // the description, then sync the new description
2192  // to players in the lobby.
2193  g.process_turn(data, p);
2195 
2196  return;
2197  } else if(data.child("whiteboard")) {
2198  g.process_whiteboard(data, p);
2199  return;
2200  } else if(data.child("change_turns_wml")) {
2201  g.process_change_turns_wml(data, p);
2203  return;
2204  } else if(simple_wml::node* sch = data.child("request_choice")) {
2205  g.handle_choice(*sch, p);
2206  return;
2207  } else if(data.child("message")) {
2208  g.process_message(data, p);
2209  return;
2210  } else if(data.child("stop_updates")) {
2211  g.send_data(data, p);
2212  return;
2213  // Data to ignore.
2214  } else if(
2215  data.child("error") ||
2216  data.child("side_secured") ||
2217  data.root().has_attr("failed") ||
2218  data.root().has_attr("side")
2219  ) {
2220  return;
2221  }
2222 
2223  WRN_SERVER << p->client_ip() << "\tReceived unknown data from: " << player.name()
2224  << " in game: \"" << g.name() << "\" (" << g.id() << ", " << g.db_id() << ")\n"
2225  << data.output();
2226 }
2227 
2228 template<class SocketPtr> void server::send_server_message(SocketPtr socket, const std::string& message, const std::string& type)
2229 {
2230  simple_wml::document server_message;
2231  simple_wml::node& msg = server_message.root().add_child("message");
2232  msg.set_attr("sender", "server");
2233  msg.set_attr_esc("message", message);
2234  msg.set_attr_dup("type", type.c_str());
2235 
2236  async_send_doc_queued(socket, server_message);
2237 }
2238 
2240 {
2241  utils::visit([](auto&& socket) {
2242  if constexpr (utils::decayed_is_same<tls_socket_ptr, decltype(socket)>) {
2243  socket->async_shutdown([socket](...) {});
2244  const char buffer[] = "";
2245  async_write(*socket, boost::asio::buffer(buffer), [socket](...) { socket->lowest_layer().close(); });
2246  } else {
2247  socket->lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_receive);
2248  }
2249  }, player->socket());
2250 }
2251 
2253 {
2254  std::string ip = iter->client_ip();
2255 
2256  const std::shared_ptr<game> g = iter->get_game();
2257  bool game_ended = false;
2258  if(g) {
2259  game_ended = g->remove_player(iter, true, false);
2260  }
2261 
2263  const std::size_t index =
2264  std::distance(users.begin(), std::find(users.begin(), users.end(), iter->info().config_address()));
2265 
2266  // Notify other players in lobby
2267  simple_wml::document diff;
2268  if(make_delete_diff(games_and_users_list_.root(), nullptr, "user", iter->info().config_address(), diff)) {
2269  send_to_lobby(diff, iter);
2270  }
2271 
2273 
2274  LOG_SERVER << ip << "\t" << iter->info().name() << "\thas logged off";
2275 
2276  // Find the matching nick-ip pair in the log and update the sign off time
2277  if(user_handler_) {
2278  user_handler_->db_update_logout(iter->info().get_login_id());
2279  } else {
2280  connection_log ip_name { iter->info().name(), ip, {} };
2281 
2282  auto i = std::find(ip_log_.begin(), ip_log_.end(), ip_name);
2283  if(i != ip_log_.end()) {
2284  i->log_off = std::chrono::system_clock::now();
2285  }
2286  }
2287 
2288  for(auto& [id, queue] : queue_info_) {
2289  if(!queue.players_in_queue.empty()) {
2290  queue.players_in_queue.erase(std::remove(queue.players_in_queue.begin(), queue.players_in_queue.end(), iter->info().name()));
2291  }
2292  send_queue_update(queue, iter);
2293  }
2294 
2295  player_connections_.erase(iter);
2296 
2297  if(lan_server_ > 0s && player_connections_.size() == 0)
2299 
2300  if(game_ended) delete_game(g->id());
2301 }
2302 
2303 void server::send_to_lobby(simple_wml::document& data, utils::optional<player_iterator> exclude)
2304 {
2305  for(const auto& p : player_connections_.get<game_t>().equal_range(0)) {
2306  auto player { player_connections_.iterator_to(p) };
2307  if(player != exclude) {
2309  }
2310  }
2311 }
2312 
2313 void server::send_server_message_to_lobby(const std::string& message, utils::optional<player_iterator> exclude)
2314 {
2315  for(const auto& p : player_connections_.get<game_t>().equal_range(0)) {
2316  auto player { player_connections_.iterator_to(p) };
2317  if(player != exclude) {
2318  send_server_message(player, message, "alert");
2319  }
2320  }
2321 }
2322 
2323 void server::send_server_message_to_all(const std::string& message, utils::optional<player_iterator> exclude)
2324 {
2325  for(auto player = player_connections_.begin(); player != player_connections_.end(); ++player) {
2326  if(player != exclude) {
2327  send_server_message(player, message, "alert");
2328  }
2329  }
2330 }
2331 
2333 {
2334  if(restart_command.empty()) {
2335  return;
2336  }
2337 
2338  // Example config line:
2339  // restart_command="./wesnothd-debug -d -c ~/.wesnoth1.5/server.cfg"
2340  // remember to make new one as a daemon or it will block old one
2341  if(std::system(restart_command.c_str())) {
2342  ERR_SERVER << "Failed to start new server with command: " << restart_command;
2343  } else {
2344  LOG_SERVER << "New server started with command: " << restart_command;
2345  }
2346 }
2347 
2348 std::string server::process_command(std::string query, std::string issuer_name)
2349 {
2350  boost::trim(query);
2351 
2352  if(issuer_name == "*socket*" && !query.empty() && query.at(0) == '+') {
2353  // The first argument might be "+<issuer>: ".
2354  // In that case we use +<issuer>+ as the issuer_name.
2355  // (Mostly used for communication with IRC.)
2356  auto issuer_end = std::find(query.begin(), query.end(), ':');
2357 
2358  std::string issuer(query.begin() + 1, issuer_end);
2359  if(!issuer.empty()) {
2360  issuer_name = "+" + issuer + "+";
2361  query = std::string(issuer_end + 1, query.end());
2362  boost::trim(query);
2363  }
2364  }
2365 
2366  const auto i = std::find(query.begin(), query.end(), ' ');
2367 
2368  try {
2369  const std::string command = utf8::lowercase(std::string(query.begin(), i));
2370 
2371  std::string parameters = (i == query.end() ? "" : std::string(i + 1, query.end()));
2372  boost::trim(parameters);
2373 
2374  std::ostringstream out;
2375  auto handler_itor = cmd_handlers_.find(command);
2376 
2377  if(handler_itor == cmd_handlers_.end()) {
2378  out << "Command '" << command << "' is not recognized.\n" << help_msg;
2379  } else {
2380  const cmd_handler& handler = handler_itor->second;
2381  try {
2382  handler(issuer_name, query, parameters, &out);
2383  } catch(const std::bad_function_call& ex) {
2384  ERR_SERVER << "While handling a command '" << command
2385  << "', caught a std::bad_function_call exception.";
2386  ERR_SERVER << ex.what();
2387  out << "An internal server error occurred (std::bad_function_call) while executing '" << command
2388  << "'\n";
2389  }
2390  }
2391 
2392  return out.str();
2393 
2394  } catch(const utf8::invalid_utf8_exception& e) {
2395  std::string msg = "While handling a command, caught an invalid utf8 exception: ";
2396  msg += e.what();
2397  ERR_SERVER << msg;
2398  return (msg + '\n');
2399  }
2400 }
2401 
2402 // Shutdown, restart and sample commands can only be issued via the socket.
2404  const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream* out)
2405 {
2406  assert(out != nullptr);
2407 
2408  if(issuer_name != "*socket*" && !allow_remote_shutdown_) {
2409  *out << denied_msg;
2410  return;
2411  }
2412 
2413  if(parameters == "now") {
2414  BOOST_THROW_EXCEPTION(server_shutdown("shut down by admin command"));
2415  } else {
2416  // Graceful shut down.
2417  graceful_restart = true;
2418  acceptor_v6_.close();
2419  acceptor_v4_.close();
2420 
2421  timer_.expires_after(10s);
2422  timer_.async_wait(std::bind(&server::handle_graceful_timeout, this, std::placeholders::_1));
2423 
2425  "msg The server is shutting down. You may finish your games but can't start new ones. Once all "
2426  "games have ended the server will exit.",
2427  issuer_name
2428  );
2429 
2430  *out << "Server is doing graceful shut down.";
2431  }
2432 }
2433 
2434 void server::restart_handler(const std::string& issuer_name,
2435  const std::string& /*query*/,
2436  std::string& /*parameters*/,
2437  std::ostringstream* out)
2438 {
2439  assert(out != nullptr);
2440 
2441  if(issuer_name != "*socket*" && !allow_remote_shutdown_) {
2442  *out << denied_msg;
2443  return;
2444  }
2445 
2446  if(restart_command.empty()) {
2447  *out << "No restart_command configured! Not restarting.";
2448  } else {
2449  graceful_restart = true;
2450  acceptor_v6_.close();
2451  acceptor_v4_.close();
2452  timer_.expires_after(10s);
2453  timer_.async_wait(std::bind(&server::handle_graceful_timeout, this, std::placeholders::_1));
2454 
2455  start_new_server();
2456 
2458  "msg The server has been restarted. You may finish current games but can't start new ones and "
2459  "new players can't join this (old) server instance. (So if a player of your game disconnects "
2460  "you have to save, reconnect and reload the game on the new server instance. It is actually "
2461  "recommended to do that right away.)",
2462  issuer_name
2463  );
2464 
2465  *out << "New server started.";
2466  }
2467 }
2468 
2470  const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream* out)
2471 {
2472  assert(out != nullptr);
2473 
2474  if(parameters.empty()) {
2475  *out << "Current sample frequency: " << request_sample_frequency;
2476  return;
2477  } else if(issuer_name != "*socket*") {
2478  *out << denied_msg;
2479  return;
2480  }
2481 
2482  request_sample_frequency = atoi(parameters.c_str());
2483  if(request_sample_frequency <= 0) {
2484  *out << "Sampling turned off.";
2485  } else {
2486  *out << "Sampling every " << request_sample_frequency << " requests.";
2487  }
2488 }
2489 
2490 void server::help_handler(const std::string& /*issuer_name*/,
2491  const std::string& /*query*/,
2492  std::string& /*parameters*/,
2493  std::ostringstream* out)
2494 {
2495  assert(out != nullptr);
2496  *out << help_msg;
2497 }
2498 
2499 void server::stats_handler(const std::string& /*issuer_name*/,
2500  const std::string& /*query*/,
2501  std::string& /*parameters*/,
2502  std::ostringstream* out)
2503 {
2504  assert(out != nullptr);
2505 
2506  *out << "Number of games = " << games().size() << "\nTotal number of users = " << player_connections_.size();
2507 }
2508 
2509 void server::metrics_handler(const std::string& /*issuer_name*/,
2510  const std::string& /*query*/,
2511  std::string& /*parameters*/,
2512  std::ostringstream* out)
2513 {
2514  assert(out != nullptr);
2515  *out << metrics_;
2516 }
2517 
2518 void server::requests_handler(const std::string& /*issuer_name*/,
2519  const std::string& /*query*/,
2520  std::string& /*parameters*/,
2521  std::ostringstream* out)
2522 {
2523  assert(out != nullptr);
2524  metrics_.requests(*out);
2525 }
2526 
2527 void server::roll_handler(const std::string& issuer_name,
2528  const std::string& /*query*/,
2529  std::string& parameters,
2530  std::ostringstream* out)
2531 {
2532  assert(out != nullptr);
2533  if(parameters.empty()) {
2534  return;
2535  }
2536 
2537  int N;
2538  try {
2539  N = std::stoi(parameters);
2540  } catch(const std::invalid_argument&) {
2541  *out << "The number of die sides must be a number!";
2542  return;
2543  } catch(const std::out_of_range&) {
2544  *out << "The number of sides is too big for the die!";
2545  return;
2546  }
2547 
2548  if(N < 1) {
2549  *out << "The die cannot have less than 1 side!";
2550  return;
2551  }
2552  std::uniform_int_distribution<int> dice_distro(1, N);
2553  std::string value = std::to_string(dice_distro(die_));
2554 
2555  *out << "You rolled a die [1 - " + parameters + "] and got a " + value + ".";
2556 
2557  auto player_ptr = player_connections_.get<name_t>().find(issuer_name);
2558  if(player_ptr == player_connections_.get<name_t>().end()) {
2559  return;
2560  }
2561 
2562  auto g_ptr = player_ptr->get_game();
2563  if(g_ptr) {
2564  g_ptr->send_server_message_to_all(issuer_name + " rolled a die [1 - " + parameters + "] and got a " + value + ".", player_connections_.project<0>(player_ptr));
2565  } else {
2566  *out << " (The result is shown to others only in a game.)";
2567  }
2568 }
2569 
2570 void server::games_handler(const std::string& /*issuer_name*/,
2571  const std::string& /*query*/,
2572  std::string& /*parameters*/,
2573  std::ostringstream* out)
2574 {
2575  assert(out != nullptr);
2576  metrics_.games(*out);
2577 }
2578 
2579 void server::wml_handler(const std::string& /*issuer_name*/,
2580  const std::string& /*query*/,
2581  std::string& /*parameters*/,
2582  std::ostringstream* out)
2583 {
2584  assert(out != nullptr);
2585  *out << simple_wml::document::stats();
2586 }
2587 
2589  const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream* out)
2590 {
2591  assert(out != nullptr);
2592 
2593  if(parameters.empty()) {
2594  *out << "You must type a message.";
2595  return;
2596  }
2597 
2598  const std::string& sender = issuer_name;
2599  const std::string& message = parameters;
2600  LOG_SERVER << "Admin message: <" << sender
2601  << (message.find("/me ") == 0 ? std::string(message.begin() + 3, message.end()) + ">" : "> " + message);
2602 
2604  simple_wml::node& msg = data.root().add_child("whisper");
2605  msg.set_attr_dup("sender", ("admin message from " + sender).c_str());
2606  msg.set_attr_dup("message", message.c_str());
2607 
2608  int n = 0;
2609  for(const auto& player : player_connections_) {
2610  if(player.info().is_moderator()) {
2611  ++n;
2613  }
2614  }
2615 
2616  bool is_admin = false;
2617 
2618  for(const auto& player : player_connections_) {
2619  if(issuer_name == player.info().name() && player.info().is_moderator()) {
2620  is_admin = true;
2621  break;
2622  }
2623  }
2624 
2625  if(!is_admin) {
2626  *out << "Your report has been logged and sent to the server administrators. Thanks!";
2627  return;
2628  }
2629 
2630  *out << "Your report has been logged and sent to " << n << " online administrators. Thanks!";
2631 }
2632 
2634  const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream* out)
2635 {
2636  assert(out != nullptr);
2637 
2638  auto first_space = std::find(parameters.begin(), parameters.end(), ' ');
2639  if(first_space == parameters.end()) {
2640  *out << "You must name a receiver.";
2641  return;
2642  }
2643 
2644  const std::string& sender = issuer_name;
2645  const std::string receiver(parameters.begin(), first_space);
2646 
2647  std::string message(first_space + 1, parameters.end());
2648  boost::trim(message);
2649 
2650  if(message.empty()) {
2651  *out << "You must type a message.";
2652  return;
2653  }
2654 
2656  simple_wml::node& msg = data.root().add_child("whisper");
2657 
2658  // This string is parsed by the client!
2659  msg.set_attr_dup("sender", ("server message from " + sender).c_str());
2660  msg.set_attr_dup("message", message.c_str());
2661 
2662  for(const auto& player : player_connections_) {
2663  if(receiver != player.info().name().c_str()) {
2664  continue;
2665  }
2666 
2668  *out << "Message to " << receiver << " successfully sent.";
2669  return;
2670  }
2671 
2672  *out << "No such nick: " << receiver;
2673 }
2674 
2675 void server::msg_handler(const std::string& /*issuer_name*/,
2676  const std::string& /*query*/,
2677  std::string& parameters,
2678  std::ostringstream* out)
2679 {
2680  assert(out != nullptr);
2681 
2682  if(parameters.empty()) {
2683  *out << "You must type a message.";
2684  return;
2685  }
2686 
2687  send_server_message_to_all(parameters);
2688 
2689  LOG_SERVER << "<server"
2690  << (parameters.find("/me ") == 0
2691  ? std::string(parameters.begin() + 3, parameters.end()) + ">"
2692  : "> " + parameters);
2693 
2694  *out << "message '" << parameters << "' relayed to players";
2695 }
2696 
2697 void server::lobbymsg_handler(const std::string& /*issuer_name*/,
2698  const std::string& /*query*/,
2699  std::string& parameters,
2700  std::ostringstream* out)
2701 {
2702  assert(out != nullptr);
2703 
2704  if(parameters.empty()) {
2705  *out << "You must type a message.";
2706  return;
2707  }
2708 
2709  send_server_message_to_lobby(parameters);
2710  LOG_SERVER << "<server"
2711  << (parameters.find("/me ") == 0
2712  ? std::string(parameters.begin() + 3, parameters.end()) + ">"
2713  : "> " + parameters);
2714 
2715  *out << "message '" << parameters << "' relayed to players";
2716 }
2717 
2719  const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream* out)
2720 {
2721  assert(out != nullptr);
2722 
2723  if(parameters.empty()) {
2724  *out << "Server version is " << game_config::wesnoth_version.str();
2725  return;
2726  }
2727 
2728  for(const auto& player : player_connections_) {
2729  if(parameters == player.info().name()) {
2730  *out << "Player " << parameters << " is using wesnoth " << player.info().version();
2731  return;
2732  }
2733  }
2734 
2735  *out << "Player '" << parameters << "' not found.";
2736 }
2737 
2739  const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream* out)
2740 {
2741  assert(out != nullptr);
2742 
2743  *out << "STATUS REPORT for '" << parameters << "'";
2744  bool found_something = false;
2745 
2746  // If a simple username is given we'll check for its IP instead.
2747  if(utils::isvalid_username(parameters)) {
2748  for(const auto& player : player_connections_) {
2749  if(parameters == player.name()) {
2750  parameters = player.client_ip();
2751  found_something = true;
2752  break;
2753  }
2754  }
2755 
2756  if(!found_something) {
2757  // out << "\nNo match found. You may want to check with 'searchlog'.";
2758  // return out.str();
2759  *out << process_command("searchlog " + parameters, issuer_name);
2760  return;
2761  }
2762  }
2763 
2764  const bool match_ip = ((std::count(parameters.begin(), parameters.end(), '.') >= 1) || (std::count(parameters.begin(), parameters.end(), ':') >= 1));
2765  for(const auto& player : player_connections_) {
2766  if(parameters.empty() || parameters == "*" ||
2767  (match_ip && utils::wildcard_string_match(player.client_ip(), parameters)) ||
2768  (!match_ip && utils::wildcard_string_match(utf8::lowercase(player.info().name()), utf8::lowercase(parameters)))
2769  ) {
2770  found_something = true;
2771  *out << std::endl << player_status(player);
2772  }
2773  }
2774 
2775  if(!found_something) {
2776  *out << "\nNo match found. You may want to check with 'searchlog'.";
2777  }
2778 }
2779 
2780 void server::clones_handler(const std::string& /*issuer_name*/,
2781  const std::string& /*query*/,
2782  std::string& /*parameters*/,
2783  std::ostringstream* out)
2784 {
2785  assert(out != nullptr);
2786  *out << "CLONES STATUS REPORT";
2787 
2788  std::set<std::string> clones;
2789 
2790  for(auto it = player_connections_.begin(); it != player_connections_.end(); ++it) {
2791  if(clones.find(it->client_ip()) != clones.end()) {
2792  continue;
2793  }
2794 
2795  bool found = false;
2796  for(auto clone = std::next(it); clone != player_connections_.end(); ++clone) {
2797  if(it->client_ip() == clone->client_ip()) {
2798  if(!found) {
2799  found = true;
2800  clones.insert(it->client_ip());
2801  *out << std::endl << player_status(*it);
2802  }
2803 
2804  *out << std::endl << player_status(*clone);
2805  }
2806  }
2807  }
2808 
2809  if(clones.empty()) {
2810  *out << std::endl << "No clones found.";
2811  }
2812 }
2813 
2814 void server::bans_handler(const std::string& /*issuer_name*/,
2815  const std::string& /*query*/,
2816  std::string& parameters,
2817  std::ostringstream* out)
2818 {
2819  assert(out != nullptr);
2820 
2821  try {
2822  if(parameters.empty()) {
2823  ban_manager_.list_bans(*out);
2824  } else if(utf8::lowercase(parameters) == "deleted") {
2826  } else if(utf8::lowercase(parameters).find("deleted") == 0) {
2827  std::string mask = parameters.substr(7);
2828  ban_manager_.list_deleted_bans(*out, boost::trim_copy(mask));
2829  } else {
2830  boost::trim(parameters);
2831  ban_manager_.list_bans(*out, parameters);
2832  }
2833 
2834  } catch(const utf8::invalid_utf8_exception& e) {
2835  ERR_SERVER << "While handling bans, caught an invalid utf8 exception: " << e.what();
2836  }
2837 }
2838 
2840  const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream* out)
2841 {
2842  assert(out != nullptr);
2843 
2844  bool banned = false;
2845  auto first_space = std::find(parameters.begin(), parameters.end(), ' ');
2846 
2847  if(first_space == parameters.end()) {
2848  *out << ban_manager_.get_ban_help();
2849  return;
2850  }
2851 
2852  auto second_space = std::find(first_space + 1, parameters.end(), ' ');
2853  const std::string target(parameters.begin(), first_space);
2854  const std::string duration(first_space + 1, second_space);
2855  auto [success, parsed_time] = ban_manager_.parse_time(duration, std::chrono::system_clock::now());
2856 
2857  if(!success) {
2858  *out << "Failed to parse the ban duration: '" << duration << "'\n" << ban_manager_.get_ban_help();
2859  return;
2860  }
2861 
2862  if(second_space == parameters.end()) {
2863  --second_space;
2864  }
2865 
2866  std::string reason(second_space + 1, parameters.end());
2867  boost::trim(reason);
2868 
2869  if(reason.empty()) {
2870  *out << "You need to give a reason for the ban.";
2871  return;
2872  }
2873 
2874  std::string dummy_group;
2875 
2876  // if we find a '.' consider it an ip mask
2877  /** @todo FIXME: make a proper check for valid IPs. */
2878  if(std::count(target.begin(), target.end(), '.') >= 1) {
2879  banned = true;
2880 
2881  *out << ban_manager_.ban(target, parsed_time, reason, issuer_name, dummy_group);
2882  } else {
2883  for(const auto& player : player_connections_) {
2884  if(utils::wildcard_string_match(player.info().name(), target)) {
2885  if(banned) {
2886  *out << "\n";
2887  } else {
2888  banned = true;
2889  }
2890 
2891  const std::string ip = player.client_ip();
2892  *out << ban_manager_.ban(ip, parsed_time, reason, issuer_name, dummy_group, target);
2893  }
2894  }
2895 
2896  if(!banned) {
2897  *out << "Nickname mask '" << target << "' did not match, no bans set.";
2898  }
2899  }
2900 }
2901 
2903  const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream* out)
2904 {
2905  assert(out != nullptr);
2906 
2907  bool banned = false;
2908  auto first_space = std::find(parameters.begin(), parameters.end(), ' ');
2909  if(first_space == parameters.end()) {
2910  *out << ban_manager_.get_ban_help();
2911  return;
2912  }
2913 
2914  auto second_space = std::find(first_space + 1, parameters.end(), ' ');
2915  const std::string target(parameters.begin(), first_space);
2916  const std::string duration(first_space + 1, second_space);
2917  auto [success, parsed_time] = ban_manager_.parse_time(duration, std::chrono::system_clock::now());
2918 
2919  if(!success) {
2920  *out << "Failed to parse the ban duration: '" << duration << "'\n" << ban_manager_.get_ban_help();
2921  return;
2922  }
2923 
2924  if(second_space == parameters.end()) {
2925  --second_space;
2926  }
2927 
2928  std::string reason(second_space + 1, parameters.end());
2929  boost::trim(reason);
2930 
2931  if(reason.empty()) {
2932  *out << "You need to give a reason for the ban.";
2933  return;
2934  }
2935 
2936  std::string dummy_group;
2937  std::vector<player_iterator> users_to_kick;
2938 
2939  // if we find a '.' consider it an ip mask
2940  /** @todo FIXME: make a proper check for valid IPs. */
2941  if(std::count(target.begin(), target.end(), '.') >= 1) {
2942  banned = true;
2943 
2944  *out << ban_manager_.ban(target, parsed_time, reason, issuer_name, dummy_group);
2945 
2947  if(utils::wildcard_string_match(player->client_ip(), target)) {
2948  users_to_kick.push_back(player);
2949  }
2950  }
2951  } else {
2953  if(utils::wildcard_string_match(player->info().name(), target)) {
2954  if(banned) {
2955  *out << "\n";
2956  } else {
2957  banned = true;
2958  }
2959 
2960  const std::string ip = player->client_ip();
2961  *out << ban_manager_.ban(ip, parsed_time, reason, issuer_name, dummy_group, target);
2962  users_to_kick.push_back(player);
2963  }
2964  }
2965 
2966  if(!banned) {
2967  *out << "Nickname mask '" << target << "' did not match, no bans set.";
2968  }
2969  }
2970 
2971  for(auto user : users_to_kick) {
2972  *out << "\nKicked " << user->info().name() << " (" << user->client_ip() << ").";
2973  utils::visit([this,reason](auto&& socket) { async_send_error(socket, "You have been banned. Reason: " + reason); }, user->socket());
2974  disconnect_player(user);
2975  }
2976 }
2977 
2979  const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream* out)
2980 {
2981  assert(out != nullptr);
2982 
2983  bool banned = false;
2984  auto first_space = std::find(parameters.begin(), parameters.end(), ' ');
2985  if(first_space == parameters.end()) {
2986  *out << ban_manager_.get_ban_help();
2987  return;
2988  }
2989 
2990  auto second_space = std::find(first_space + 1, parameters.end(), ' ');
2991  const std::string target(parameters.begin(), first_space);
2992 
2993  std::string group = std::string(first_space + 1, second_space);
2994  first_space = second_space;
2995  second_space = std::find(first_space + 1, parameters.end(), ' ');
2996 
2997  const std::string duration(first_space + 1, second_space);
2998  auto [success, parsed_time] = ban_manager_.parse_time(duration, std::chrono::system_clock::now());
2999 
3000  if(!success) {
3001  *out << "Failed to parse the ban duration: '" << duration << "'\n" << ban_manager_.get_ban_help();
3002  return;
3003  }
3004 
3005  if(second_space == parameters.end()) {
3006  --second_space;
3007  }
3008 
3009  std::string reason(second_space + 1, parameters.end());
3010  boost::trim(reason);
3011 
3012  if(reason.empty()) {
3013  *out << "You need to give a reason for the ban.";
3014  return;
3015  }
3016 
3017  // if we find a '.' consider it an ip mask
3018  /** @todo FIXME: make a proper check for valid IPs. */
3019  if(std::count(target.begin(), target.end(), '.') >= 1) {
3020  banned = true;
3021 
3022  *out << ban_manager_.ban(target, parsed_time, reason, issuer_name, group);
3023  } else {
3024  for(const auto& player : player_connections_) {
3025  if(utils::wildcard_string_match(player.info().name(), target)) {
3026  if(banned) {
3027  *out << "\n";
3028  } else {
3029  banned = true;
3030  }
3031 
3032  const std::string ip = player.client_ip();
3033  *out << ban_manager_.ban(ip, parsed_time, reason, issuer_name, group, target);
3034  }
3035  }
3036 
3037  if(!banned) {
3038  *out << "Nickname mask '" << target << "' did not match, no bans set.";
3039  }
3040  }
3041 }
3042 
3043 void server::unban_handler(const std::string& /*issuer_name*/,
3044  const std::string& /*query*/,
3045  std::string& parameters,
3046  std::ostringstream* out)
3047 {
3048  assert(out != nullptr);
3049 
3050  if(parameters.empty()) {
3051  *out << "You must enter an ipmask to unban.";
3052  return;
3053  }
3054 
3055  ban_manager_.unban(*out, parameters);
3056 }
3057 
3058 void server::ungban_handler(const std::string& /*issuer_name*/,
3059  const std::string& /*query*/,
3060  std::string& parameters,
3061  std::ostringstream* out)
3062 {
3063  assert(out != nullptr);
3064 
3065  if(parameters.empty()) {
3066  *out << "You must enter an ipmask to ungban.";
3067  return;
3068  }
3069 
3070  ban_manager_.unban_group(*out, parameters);
3071 }
3072 
3073 void server::kick_handler(const std::string& /*issuer_name*/,
3074  const std::string& /*query*/,
3075  std::string& parameters,
3076  std::ostringstream* out)
3077 {
3078  assert(out != nullptr);
3079 
3080  if(parameters.empty()) {
3081  *out << "You must enter a mask to kick.";
3082  return;
3083  }
3084 
3085  auto i = std::find(parameters.begin(), parameters.end(), ' ');
3086  const std::string kick_mask = std::string(parameters.begin(), i);
3087  const std::string kick_message = (i == parameters.end()
3088  ? "You have been kicked."
3089  : "You have been kicked. Reason: " + std::string(i + 1, parameters.end()));
3090 
3091  bool kicked = false;
3092 
3093  // if we find a '.' consider it an ip mask
3094  const bool match_ip = (std::count(kick_mask.begin(), kick_mask.end(), '.') >= 1);
3095 
3096  std::vector<player_iterator> users_to_kick;
3098  if((match_ip && utils::wildcard_string_match(player->client_ip(), kick_mask)) ||
3099  (!match_ip && utils::wildcard_string_match(player->info().name(), kick_mask))
3100  ) {
3101  users_to_kick.push_back(player);
3102  }
3103  }
3104 
3105  for(const auto& player : users_to_kick) {
3106  if(kicked) {
3107  *out << "\n";
3108  } else {
3109  kicked = true;
3110  }
3111 
3112  *out << "Kicked " << player->name() << " (" << player->client_ip() << "). '"
3113  << kick_message << "'";
3114 
3115  utils::visit([this, &kick_message](auto&& socket) { async_send_error(socket, kick_message); }, player->socket());
3117  }
3118 
3119  if(!kicked) {
3120  *out << "No user matched '" << kick_mask << "'.";
3121  }
3122 }
3123 
3124 void server::motd_handler(const std::string& /*issuer_name*/,
3125  const std::string& /*query*/,
3126  std::string& parameters,
3127  std::ostringstream* out)
3128 {
3129  assert(out != nullptr);
3130 
3131  if(parameters.empty()) {
3132  if(!motd_.empty()) {
3133  *out << "Message of the day:\n" << motd_;
3134  return;
3135  } else {
3136  *out << "No message of the day set.";
3137  return;
3138  }
3139  }
3140 
3141  motd_ = parameters;
3142  *out << "Message of the day set to: " << motd_;
3143 }
3144 
3145 void server::searchlog_handler(const std::string& /*issuer_name*/,
3146  const std::string& /*query*/,
3147  std::string& parameters,
3148  std::ostringstream* out)
3149 {
3150  assert(out != nullptr);
3151 
3152  if(parameters.empty()) {
3153  *out << "You must enter a mask to search for.";
3154  return;
3155  }
3156 
3157  *out << "IP/NICK LOG for '" << parameters << "'";
3158 
3159  // If this looks like an IP look up which nicks have been connected from it
3160  // Otherwise look for the last IP the nick used to connect
3161  const bool match_ip = (std::count(parameters.begin(), parameters.end(), '.') >= 1);
3162 
3163  if(!user_handler_) {
3164  bool found_something = false;
3165 
3166  for(const auto& i : ip_log_) {
3167  const std::string& username = i.nick;
3168  const std::string& ip = i.ip;
3169 
3170  if((match_ip && utils::wildcard_string_match(ip, parameters)) ||
3171  (!match_ip && utils::wildcard_string_match(utf8::lowercase(username), utf8::lowercase(parameters)))
3172  ) {
3173  found_something = true;
3174  auto player = player_connections_.get<name_t>().find(username);
3175 
3176  if(player != player_connections_.get<name_t>().end() && player->client_ip() == ip) {
3177  *out << std::endl << player_status(*player);
3178  } else {
3179  *out << "\n'" << username << "' @ " << ip
3180  << " last seen: " << chrono::format_local_timestamp(i.log_off, "%H:%M:%S %d.%m.%Y");
3181  }
3182  }
3183  }
3184 
3185  if(!found_something) {
3186  *out << "\nNo match found.";
3187  }
3188  } else {
3189  if(!match_ip) {
3190  utils::to_sql_wildcards(parameters);
3191  user_handler_->get_ips_for_user(parameters, out);
3192  } else {
3193  user_handler_->get_users_for_ip(parameters, out);
3194  }
3195  }
3196 }
3197 
3198 void server::dul_handler(const std::string& /*issuer_name*/,
3199  const std::string& /*query*/,
3200  std::string& parameters,
3201  std::ostringstream* out)
3202 {
3203  assert(out != nullptr);
3204 
3205  try {
3206  if(parameters.empty()) {
3207  *out << "Unregistered login is " << (deny_unregistered_login_ ? "disallowed" : "allowed") << ".";
3208  } else {
3209  deny_unregistered_login_ = (utf8::lowercase(parameters) == "yes");
3210  *out << "Unregistered login is now " << (deny_unregistered_login_ ? "disallowed" : "allowed") << ".";
3211  }
3212 
3213  } catch(const utf8::invalid_utf8_exception& e) {
3214  ERR_SERVER << "While handling dul (deny unregistered logins), caught an invalid utf8 exception: " << e.what();
3215  }
3216 }
3217 
3218 void server::stopgame_handler(const std::string& /*issuer_name*/,
3219  const std::string& /*query*/,
3220  std::string& parameters,
3221  std::ostringstream* out)
3222 {
3223  assert(out != nullptr);
3224 
3225  const std::string nick = parameters.substr(0, parameters.find(' '));
3226  const std::string reason = parameters.length() > nick.length()+1 ? parameters.substr(nick.length()+1) : "";
3227  auto player = player_connections_.get<name_t>().find(nick);
3228 
3229  if(player != player_connections_.get<name_t>().end()){
3230  std::shared_ptr<game> g = player->get_game();
3231  if(g){
3232  *out << "Player '" << nick << "' is in game with id '" << g->id() << ", " << g->db_id() << "' named '" << g->name() << "'. Ending game for reason: '" << reason << "'...";
3233  delete_game(g->id(), reason);
3234  } else {
3235  *out << "Player '" << nick << "' is not currently in a game.";
3236  }
3237  } else {
3238  *out << "Player '" << nick << "' is not currently logged in.";
3239  }
3240 }
3241 
3242 void server::reset_queues_handler(const std::string& /*issuer_name*/,
3243  const std::string& /*query*/,
3244  std::string& /*parameters*/,
3245  std::ostringstream* out)
3246 {
3247  assert(out != nullptr);
3248 
3250  player->info().clear_queues();
3251  }
3252 
3253  for(auto& [id, queue] : queue_info_) {
3254  queue.players_in_queue.clear();
3255  send_queue_update(queue);
3256  }
3257 
3258  *out << "Reset all queues";
3259 }
3260 
3261 void server::delete_game(int gameid, const std::string& reason)
3262 {
3263  // Set the availability status for all quitting users.
3264  auto range_pair = player_connections_.get<game_t>().equal_range(gameid);
3265 
3266  // Make a copy of the iterators so that we can change them while iterating over them.
3267  // We can use pair::first_type since equal_range returns a pair of iterators.
3268  std::vector<decltype(range_pair)::first_type> range_vctor;
3269 
3270  for(auto it = range_pair.first; it != range_pair.second; ++it) {
3271  range_vctor.push_back(it);
3272  it->info().mark_available();
3273 
3274  simple_wml::document udiff;
3275  if(make_change_diff(games_and_users_list_.root(), nullptr, "user", it->info().config_address(), udiff)) {
3276  send_to_lobby(udiff);
3277  } else {
3278  ERR_SERVER << "ERROR: delete_game(): Could not find user in players_.";
3279  }
3280  }
3281 
3282  // Put the remaining users back in the lobby.
3283  // This will call cleanup_game() deleter since there won't
3284  // be any references to that game from player_connections_ anymore
3285  for(const auto& it : range_vctor) {
3286  player_connections_.get<game_t>().modify(it, std::bind(&player_record::enter_lobby, std::placeholders::_1));
3287  }
3288 
3289  // send users in the game a notification to leave the game since it has ended
3290  static simple_wml::document leave_game_doc("[leave_game]\n[/leave_game]\n", simple_wml::INIT_COMPRESSED);
3291 
3292  for(const auto& it : range_vctor) {
3293  player_iterator p { player_connections_.project<0>(it) };
3294  if(reason != "") {
3295  simple_wml::document leave_game_doc_reason("[leave_game]\n[/leave_game]\n", simple_wml::INIT_STATIC);
3296  leave_game_doc_reason.child("leave_game")->set_attr_dup("reason", reason.c_str());
3297  send_to_player(p, leave_game_doc_reason);
3298  } else {
3299  send_to_player(p, leave_game_doc);
3300  }
3302  }
3303 }
3304 
3305 void server::update_game_in_lobby(wesnothd::game& g, utils::optional<player_iterator> exclude)
3306 {
3307  simple_wml::document diff;
3308  if(auto p_desc = g.changed_description()) {
3309  if(make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", p_desc, diff)) {
3310  send_to_lobby(diff, exclude);
3311  }
3312  }
3313 }
3314 
3315 } // namespace wesnothd
3316 
3317 int main(int argc, char** argv)
3318 {
3319  int port = 15000;
3320  bool keep_alive = false;
3321 
3322  srand(static_cast<unsigned>(std::time(nullptr)));
3323 
3324  std::string config_file;
3325 
3326  // setting path to currentworking directory
3328 
3329  // show 'info' by default
3331  lg::timestamps(true);
3332 
3333  for(int arg = 1; arg != argc; ++arg) {
3334  const std::string val(argv[arg]);
3335  if(val.empty()) {
3336  continue;
3337  }
3338 
3339  if((val == "--config" || val == "-c") && arg + 1 != argc) {
3340  config_file = argv[++arg];
3341  } else if(val == "--verbose" || val == "-v") {
3343  } else if(val == "--dump-wml" || val == "-w") {
3344  dump_wml = true;
3345  } else if(val.substr(0, 6) == "--log-") {
3346  std::size_t p = val.find('=');
3347  if(p == std::string::npos) {
3348  PLAIN_LOG << "unknown option: " << val;
3349  return 2;
3350  }
3351 
3352  std::string s = val.substr(6, p - 6);
3354 
3355  if(s == "error") {
3357  } else if(s == "warning") {
3359  } else if(s == "info") {
3361  } else if(s == "debug") {
3363  } else {
3364  PLAIN_LOG << "unknown debug level: " << s;
3365  return 2;
3366  }
3367 
3368  while(p != std::string::npos) {
3369  std::size_t q = val.find(',', p + 1);
3370  s = val.substr(p + 1, q == std::string::npos ? q : q - (p + 1));
3371 
3373  PLAIN_LOG << "unknown debug domain: " << s;
3374  return 2;
3375  }
3376 
3377  p = q;
3378  }
3379  } else if((val == "--port" || val == "-p") && arg + 1 != argc) {
3380  port = atoi(argv[++arg]);
3381  } else if(val == "--keepalive") {
3382  keep_alive = true;
3383  } else if(val == "--help" || val == "-h") {
3384  std::cout << "usage: " << argv[0]
3385  << " [-dvwV] [-c path] [-p port]\n"
3386  << " -c, --config <path> Tells wesnothd where to find the config file to use.\n"
3387  << " -d, --daemon Runs wesnothd as a daemon.\n"
3388  << " -h, --help Shows this usage message.\n"
3389  << " --log-<level>=<domain1>,<domain2>,...\n"
3390  << " sets the severity level of the debug domains.\n"
3391  << " 'all' can be used to match any debug domain.\n"
3392  << " Available levels: error, warning, info, debug.\n"
3393  << " -p, --port <port> Binds the server to the specified port.\n"
3394  << " --keepalive Enable TCP keepalive.\n"
3395  << " -v --verbose Turns on more verbose logging.\n"
3396  << " -V, --version Returns the server version.\n"
3397  << " -w, --dump-wml Print all WML sent to clients to stdout.\n";
3398  return 0;
3399  } else if(val == "--version" || val == "-V") {
3400  std::cout << "Battle for Wesnoth server " << game_config::wesnoth_version.str() << "\n";
3401  return 0;
3402  } else if(val == "--daemon" || val == "-d") {
3403 #ifdef _WIN32
3404  ERR_SERVER << "Running as a daemon is not supported on this platform";
3405  return -1;
3406 #else
3407  const pid_t pid = fork();
3408  if(pid < 0) {
3409  ERR_SERVER << "Could not fork and run as a daemon";
3410  return -1;
3411  } else if(pid > 0) {
3412  std::cout << "Started wesnothd as a daemon with process id " << pid << "\n";
3413  return 0;
3414  }
3415 
3416  setsid();
3417 #endif
3418  } else if(val == "--request_sample_frequency" && arg + 1 != argc) {
3419  wesnothd::request_sample_frequency = atoi(argv[++arg]);
3420  } else {
3421  ERR_SERVER << "unknown option: " << val;
3422  return 2;
3423  }
3424  }
3425 
3426  return wesnothd::server(port, keep_alive, config_file).run();
3427 }
double g
Definition: astarsearch.cpp:63
int main(int argc, char **argv)
Definition: server.cpp:2206
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:362
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:312
child_itors child_range(config_key_type key)
Definition: config.cpp:268
bool empty() const
Definition: config.cpp:845
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:380
A class to handle the non-SQL logic for connecting to the phpbb forum database.
severity get_severity() const
Definition: log.hpp:219
std::ostream & requests(std::ostream &out) const
Definition: metrics.cpp:125
std::ostream & games(std::ostream &out) const
Definition: metrics.cpp:110
void game_terminated(const std::string &reason)
Definition: metrics.cpp:105
Base class for implementing servers that use gzipped-WML network protocol.
Definition: server_base.hpp:81
void async_send_warning(const SocketPtr &socket, const std::string &msg, const char *warning_code="", const info_table &info={})
std::string hash_password(const std::string &pw, const std::string &salt, const std::string &username)
Handles hashing the password provided by the player before comparing it to the hashed password in the...
boost::asio::signal_set sighup_
boost::asio::streambuf admin_cmd_
void async_send_error(SocketPtr socket, const std::string &msg, const char *error_code="", const info_table &info={})
boost::asio::ip::tcp::acceptor acceptor_v4_
std::unique_ptr< simple_wml::document > coro_receive_doc(const SocketPtr &socket, const boost::asio::yield_context &yield)
Receive WML document from a coroutine.
boost::asio::io_context io_service_
void async_send_doc_queued(const SocketPtr &socket, simple_wml::document &doc)
High level wrapper for sending a WML document.
void read_from_fifo()
void start_server()
Definition: server_base.cpp:78
boost::asio::posix::stream_descriptor input_
void load_tls_config(const config &cfg)
void coro_send_doc(const SocketPtr &socket, simple_wml::document &doc, const boost::asio::yield_context &yield)
Send a WML document from within a coroutine.
boost::asio::ip::tcp::acceptor acceptor_v6_
node & set_attr_dup(const char *key, const char *value)
Definition: simple_wml.hpp:285
static std::string stats()
node * child(const char *name)
Definition: simple_wml.hpp:269
const string_span & attr(const char *key) const
Definition: simple_wml.hpp:132
void remove_child(const char *name, std::size_t index)
Definition: simple_wml.cpp:596
const child_list & children(const char *name) const
Definition: simple_wml.cpp:636
node & set_attr_int(const char *key, int value)
Definition: simple_wml.cpp:444
node * child(const char *name)
Definition: simple_wml.cpp:601
std::vector< node * > child_list
Definition: simple_wml.hpp:129
node & add_child(const char *name)
Definition: simple_wml.cpp:469
node & set_attr(const char *key, const char *value)
Definition: simple_wml.cpp:412
node & add_child_at(const char *name, std::size_t index)
Definition: simple_wml.cpp:450
void copy_into(node &n) const
Definition: simple_wml.cpp:814
node & set_attr_dup(const char *key, const char *value)
Definition: simple_wml.cpp:427
std::string to_string() const
Definition: simple_wml.cpp:184
const char * begin() const
Definition: simple_wml.hpp:94
const char * end() const
Definition: simple_wml.hpp:95
This class represents a single unit of a specific type.
Definition: unit.hpp:133
An interface class to handle nick registration To activate it put a [user_handler] section into the s...
@ BAN_EMAIL
Account email address ban.
@ BAN_IP
IP address ban.
@ BAN_USER
User account/name ban.
Thrown by operations encountering invalid UTF-8 data.
Represents version numbers.
std::string str() const
Serializes the version number into string form.
std::string ban(const std::string &ip, const utils::optional< std::chrono::system_clock::time_point > &end_time, const std::string &reason, const std::string &who_banned, const std::string &group, const std::string &nick="")
Definition: ban.cpp:486
void list_bans(std::ostringstream &out, const std::string &mask="*")
Definition: ban.cpp:614
void unban(std::ostringstream &os, const std::string &ip, bool immediate_write=true)
Definition: ban.cpp:523
std::pair< bool, utils::optional< std::chrono::system_clock::time_point > > parse_time(const std::string &duration, std::chrono::system_clock::time_point start_time) const
Parses the given duration and adds it to *time except if the duration is '0' or 'permanent' in which ...
Definition: ban.cpp:331
banned_ptr get_ban_info(const std::string &ip)
Definition: ban.cpp:654
void unban_group(std::ostringstream &os, const std::string &group)
Definition: ban.cpp:550
const std::string & get_ban_help() const
Definition: ban.hpp:188
void list_deleted_bans(std::ostringstream &out, const std::string &mask="*") const
Definition: ban.cpp:591
void load_config(const config &)
Definition: ban.cpp:692
static simple_wml::node * starting_pos(simple_wml::node &data)
The non-const version.
Definition: game.hpp:158
simple_wml::node * description() const
Definition: game.hpp:488
int db_id() const
This ID is not reused between scenarios of MP campaigns.
Definition: game.hpp:67
void emergency_cleanup()
Definition: game.hpp:618
int id() const
This ID is reused between scenarios of MP campaigns.
Definition: game.hpp:55
const std::string & termination_reason() const
Provides the reason the game was ended.
Definition: game.hpp:556
std::string get_replay_filename()
Definition: game.cpp:1795
void set_game(std::shared_ptr< game > new_game)
const std::shared_ptr< game > get_game() const
const std::string & version() const
Definition: player.hpp:56
const simple_wml::node * config_address() const
Definition: player.hpp:58
void set_moderator(bool moderator)
Definition: player.hpp:62
const std::set< int > & get_queues() const
Definition: player.hpp:70
const std::string & name() const
Definition: player.hpp:55
void clear_queues()
Definition: player.hpp:68
bool is_moderator() const
Definition: player.hpp:63
const std::string & source() const
Definition: player.hpp:57
utils::optional< server_base::login_ban_info > is_ip_banned(const std::string &ip)
Definition: server.cpp:649
void update_game_in_lobby(game &g, utils::optional< player_iterator > exclude={})
Definition: server.cpp:3305
void send_to_lobby(simple_wml::document &data, utils::optional< player_iterator > exclude={})
Definition: server.cpp:2303
int failed_login_limit_
Definition: server.hpp:201
void stopgame_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:3218
std::unique_ptr< user_handler > user_handler_
Definition: server.hpp:158
void handle_leave_server_queue(player_iterator p, simple_wml::node &data)
Definition: server.cpp:1723
std::string uuid_
Definition: server.hpp:167
const std::string config_file_
Definition: server.hpp:169
boost::asio::steady_timer dummy_player_timer_
Definition: server.hpp:304
void handle_message(player_iterator player, simple_wml::node &message)
Definition: server.cpp:1417
void searchlog_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:3145
std::string motd_
Definition: server.hpp:183
bool graceful_restart
Definition: server.hpp:191
std::mt19937 die_
Definition: server.hpp:160
std::vector< std::string > disallowed_names_
Definition: server.hpp:180
void clones_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2780
std::string information_
Definition: server.hpp:187
void setup_handlers()
Definition: server.cpp:374
void ban_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2839
void start_dummy_player_updates()
Definition: server.cpp:684
void load_config(bool reload)
Parse the server config into local variables.
Definition: server.cpp:435
std::string input_path_
server socket/fifo.
Definition: server.hpp:164
void unban_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:3043
std::map< std::string, config > proxy_versions_
Definition: server.hpp:179
void handle_sighup(const boost::system::error_code &error, int signal_number)
Definition: server.cpp:279
void stats_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2499
std::chrono::seconds dummy_player_timer_interval_
Definition: server.hpp:305
std::string server_id_
Definition: server.hpp:185
void delete_game(int, const std::string &reason="")
Definition: server.cpp:3261
void refresh_tournaments(const boost::system::error_code &ec)
Definition: server.cpp:733
std::string recommended_version_
Definition: server.hpp:177
void gban_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2978
void pm_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2633
std::function< void(const std::string &, const std::string &, std::string &, std::ostringstream *)> cmd_handler
Definition: server.hpp:259
void adminmsg_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2588
bool save_replays_
Definition: server.hpp:196
void start_tournaments_timer()
Definition: server.cpp:727
std::string replay_save_path_
Definition: server.hpp:197
void handle_query(player_iterator player, simple_wml::node &query)
Definition: server.cpp:1314
void handle_player_in_game(player_iterator player, simple_wml::document &doc)
Definition: server.cpp:1745
std::string restart_command
Definition: server.hpp:193
void lobbymsg_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2697
bool ip_exceeds_connection_limit(const std::string &ip) const
Definition: server.cpp:633
std::chrono::seconds failed_login_ban_
Definition: server.hpp:202
void handle_graceful_timeout(const boost::system::error_code &error)
Definition: server.cpp:292
void cleanup_game(game *)
Definition: server.cpp:1510
void handle_whisper(player_iterator player, simple_wml::node &whisper)
Definition: server.cpp:1274
metrics metrics_
Definition: server.hpp:215
void start_dump_stats()
Definition: server.cpp:666
bool is_login_allowed(boost::asio::yield_context yield, SocketPtr socket, const simple_wml::node *const login, const std::string &username, bool &registered, bool &is_moderator)
Definition: server.cpp:908
void handle_new_client(socket_ptr socket)
Definition: server.cpp:745
void status_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2738
void send_queue_update(const queue_info &queue, utils::optional< player_iterator > exclude={})
Definition: server.cpp:1444
std::string announcements_
Definition: server.hpp:184
std::deque< connection_log > ip_log_
Definition: server.hpp:120
void bans_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2814
void kick_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:3073
void dul_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:3198
void handle_join_server_queue(player_iterator p, simple_wml::node &data)
Definition: server.cpp:1665
void handle_join_game(player_iterator player, simple_wml::node &join)
Definition: server.cpp:1544
std::deque< login_log >::size_type failed_login_buffer_size_
Definition: server.hpp:203
void wml_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2579
void send_server_message(SocketPtr socket, const std::string &message, const std::string &type)
Definition: server.cpp:2228
void handle_player(boost::asio::yield_context yield, SocketPtr socket, player_iterator player)
Definition: server.cpp:1156
player_connections player_connections_
Definition: server.hpp:217
simple_wml::document games_and_users_list_
Definition: server.hpp:213
void help_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2490
void requests_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2518
void dump_stats(const boost::system::error_code &ec)
Definition: server.cpp:672
std::string tournaments_
Definition: server.hpp:186
void start_new_server()
Definition: server.cpp:2332
void remove_player(player_iterator player)
Definition: server.cpp:2252
boost::asio::steady_timer timer_
Definition: server.hpp:296
void handle_read_from_fifo(const boost::system::error_code &error, std::size_t bytes_transferred)
Definition: server.cpp:345
simple_wml::document login_response_
Definition: server.hpp:212
boost::asio::steady_timer tournaments_timer_
Definition: server.hpp:240
std::set< std::string > client_sources_
Definition: server.hpp:199
bool player_is_in_game(player_iterator player) const
Definition: server.hpp:101
void abort_lan_server_timer()
Definition: server.cpp:311
void handle_lan_server_shutdown(const boost::system::error_code &error)
Definition: server.cpp:316
bool authenticate(SocketPtr socket, const std::string &username, const std::string &password, bool name_taken, bool &registered)
Definition: server.cpp:1032
void reset_queues_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:3242
std::string admin_passwd_
Definition: server.hpp:182
void motd_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:3124
bool deny_unregistered_login_
Definition: server.hpp:195
std::vector< std::string > accepted_versions_
Definition: server.hpp:176
std::chrono::seconds lan_server_
Definition: server.hpp:192
void dummy_player_updates(const boost::system::error_code &ec)
Definition: server.cpp:690
void handle_create_game(player_iterator player, simple_wml::node &create_game)
Definition: server.cpp:1455
void send_server_message_to_lobby(const std::string &message, utils::optional< player_iterator > exclude={})
Definition: server.cpp:2313
void kickban_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2902
void send_server_message_to_all(const std::string &message, utils::optional< player_iterator > exclude={})
Definition: server.cpp:2323
void handle_nickserv(player_iterator player, simple_wml::node &nickserv)
Definition: server.cpp:1392
std::string process_command(std::string cmd, std::string issuer_name)
Process commands from admins and users.
Definition: server.cpp:2348
std::map< int, queue_info > queue_info_
Definition: server.hpp:181
void login_client(boost::asio::yield_context yield, SocketPtr socket)
Definition: server.cpp:764
std::deque< login_log > failed_logins_
Definition: server.hpp:156
std::size_t default_max_messages_
Definition: server.hpp:188
std::size_t max_ip_log_size_
Definition: server.hpp:194
void restart_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2434
void msg_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2675
boost::asio::steady_timer lan_server_timer_
Definition: server.hpp:299
void shut_down_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2403
std::map< std::string, config > redirected_versions_
Definition: server.hpp:178
void metrics_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2509
std::map< std::string, cmd_handler > cmd_handlers_
Definition: server.hpp:260
void setup_fifo()
Definition: server.cpp:324
void send_to_player(player_iterator player, simple_wml::document &data)
Definition: server.hpp:83
wesnothd::ban_manager ban_manager_
Definition: server.hpp:106
config read_config() const
Read the server config from file 'config_file_'.
Definition: server.cpp:418
boost::asio::steady_timer dump_stats_timer_
Definition: server.hpp:236
void handle_player_in_lobby(player_iterator player, simple_wml::document &doc)
Definition: server.cpp:1219
std::vector< std::string > tor_ip_list_
Definition: server.hpp:200
void roll_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2527
void games_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2570
std::deque< std::shared_ptr< game > > games() const
Definition: server.hpp:219
simple_wml::document version_query_response_
Definition: server.hpp:211
void version_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2718
std::size_t concurrent_connections_
Definition: server.hpp:190
void send_password_request(SocketPtr socket, const std::string &msg, const char *error_code="", bool force_confirmation=false)
Definition: server.cpp:1138
void sample_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:2469
void start_lan_server_timer()
Definition: server.cpp:305
void disconnect_player(player_iterator player)
Definition: server.cpp:2239
bool allow_remote_shutdown_
Definition: server.hpp:198
void ungban_handler(const std::string &, const std::string &, std::string &, std::ostringstream *)
Definition: server.cpp:3058
std::chrono::seconds default_time_period_
Definition: server.hpp:189
Definitions for the interface to Wesnoth Markup Language (WML).
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1030
Interfaces for manipulating version numbers of engine, add-ons, etc.
Standard logging facilities (interface).
#define PLAIN_LOG
Definition: log.hpp:296
Define the errors the server may send during the login procedure.
#define MP_INCORRECT_PASSWORD_ERROR
#define MP_NAME_AUTH_BAN_USER_ERROR
#define MP_MUST_LOGIN
#define MP_NAME_RESERVED_ERROR
#define MP_PASSWORD_REQUEST_FOR_LOGGED_IN_NAME
#define MP_NAME_AUTH_BAN_EMAIL_ERROR
#define MP_TOO_MANY_ATTEMPTS_ERROR
#define MP_HASHING_PASSWORD_FAILED
#define MP_SERVER_IP_BAN_ERROR
#define MP_NAME_TOO_LONG_ERROR
#define MP_NAME_AUTH_BAN_IP_ERROR
#define MP_PASSWORD_REQUEST
#define MP_NAME_INACTIVE_WARNING
#define MP_NAME_UNREGISTERED_ERROR
#define MP_INVALID_CHARS_IN_NAME_ERROR
#define MP_NAME_TAKEN_ERROR
std::string client_address(const any_socket_ptr &sock)
Definition: server.cpp:833
constexpr auto days_hours_mins_secs
Definition: chrono.hpp:98
auto parse_duration(const config_attribute_value &val, const Duration &def=Duration{0})
Definition: chrono.hpp:71
auto format_local_timestamp(const std::chrono::system_clock::time_point &time, std::string_view format="%F %T")
Definition: chrono.hpp:62
constexpr auto deconstruct_duration(const std::tuple< Ts... > &, const std::chrono::duration< Rep, Period > &span)
Definition: chrono.hpp:85
static void update()
std::string read_file(const std::string &fname)
Basic disk I/O - read file.
std::string get_cwd()
Definition: filesystem.cpp:957
void set_user_data_dir(std::string newprefdir)
Definition: filesystem.cpp:732
std::string observer
std::string path
Definition: filesystem.cpp:102
const version_info wesnoth_version(VERSION)
void remove()
Removes a tip.
Definition: tooltip.cpp:94
bool exists(const image::locator &i_locator)
Returns true if the given image actually exists, without loading it.
Definition: picture.cpp:840
config read(std::istream &in, abstract_validator *validator)
Definition: parser.cpp:600
logger & err()
Definition: log.cpp:306
severity
Definition: log.hpp:82
logger & debug()
Definition: log.cpp:324
logger & warn()
Definition: log.cpp:312
void timestamps(bool t)
Definition: log.cpp:303
logger & info()
Definition: log.cpp:318
bool set_log_domain_severity(const std::string &name, severity severity)
Definition: log.cpp:346
std::string node_to_string(const node &n)
Definition: simple_wml.cpp:802
std::string lowercase(std::string_view s)
Returns a lowercased version of the string.
Definition: unicode.cpp:50
std::string & insert(std::string &str, const std::size_t pos, const std::string &insert)
Insert a UTF-8 string at the specified position.
Definition: unicode.cpp:98
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
std::size_t index(std::string_view str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
constexpr bool decayed_is_same
Equivalent to as std::is_same_v except both types are passed through std::decay first.
Definition: general.hpp:30
void trim(std::string_view &s)
int stoi(std::string_view str)
Same interface as std::stoi and meant as a drop in replacement, except:
Definition: charconv.hpp:154
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition: general.hpp:86
bool wildcard_string_match(std::string_view str, std::string_view pat) noexcept
Performs pattern matching with wildcards.
void to_sql_wildcards(std::string &str, bool underscores)
Converts '*' to '' and optionally escapes '_'.
bool isvalid_username(const std::string &username)
Check if the username contains only valid characters.
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::vector< std::string > split(const config_attribute_value &val)
auto * find(Container &container, const Value &value)
Convenience wrapper for using find on a container without needing to comare to end()
Definition: general.hpp:140
void truncate_message(const simple_wml::string_span &str, simple_wml::node &message)
Function to ensure a text message is within the allowed length.
static void make_add_diff(const simple_wml::node &src, const char *gamelist, const char *type, simple_wml::document &out, int index=-1)
Definition: server.cpp:88
static bool make_change_diff(const simple_wml::node &src, const char *gamelist, const char *type, const simple_wml::node *item, simple_wml::document &out)
Definition: server.cpp:149
int request_sample_frequency
Definition: server.cpp:85
const std::string help_msg
Definition: server.cpp:204
player_connections::const_iterator player_iterator
static std::string player_status(const wesnothd::player_record &player)
Definition: server.cpp:190
static bool make_delete_diff(const simple_wml::node &src, const char *gamelist, const char *type, const simple_wml::node *remove, simple_wml::document &out)
Definition: server.cpp:116
version_info secure_version
Definition: server.cpp:86
const std::string denied_msg
Definition: server.cpp:203
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
std::string_view data
Definition: picture.cpp:188
static bool read_config(config &src, config &dst)
filesystem::scoped_istream preprocess_file(const std::string &fname, preproc_map *defines)
Function to use the WML preprocessor on a file.
bool dump_wml
Definition: server_base.cpp:62
std::shared_ptr< boost::asio::ssl::stream< socket_ptr::element_type > > tls_socket_ptr
Definition: server_base.hpp:52
std::string log_address(SocketPtr socket)
std::shared_ptr< boost::asio::ip::tcp::socket > socket_ptr
Definition: server_base.hpp:49
rect src
Non-transparent portion of the surface to compose.
The base template for associating string values with enum values.
Definition: enum_base.hpp:33
static constexpr utils::optional< enum_type > get_enum(const std::string_view value)
Converts a string into its enum equivalent.
Definition: enum_base.hpp:57
Ban status description.
BAN_TYPE type
Ban type.
std::chrono::seconds duration
Ban duration (0 if permanent)
std::size_t players_required
Definition: server.hpp:151
std::vector< std::string > players_in_queue
Definition: server.hpp:152
mock_party p
static map_location::direction n
static map_location::direction s
#define FIFODIR
#define DBG_SERVER
Definition: server.cpp:74
#define LOG_SERVER
normal events
Definition: server.cpp:73
#define WRN_SERVER
clients send wrong/unexpected data
Definition: server.cpp:70
#define SETUP_HANDLER(name, function)
#define ERR_CONFIG
Definition: server.cpp:77
#define ERR_SERVER
fatal and directly server related errors/warnings, ie not caused by erroneous client data
Definition: server.cpp:67
static lg::log_domain log_server("server")
static lg::log_domain log_config("config")
#define d
#define e
#define h