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