wesnoth/src/server/server.cpp

2823 lines
96 KiB
C++

/* $Id$ */
/*
Copyright (C) 2003 - 2011 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/**
* @file
* Wesnoth-Server, for multiplayer-games.
*/
#include "server.hpp"
#include "../global.hpp"
#include "../config.hpp"
#include "../foreach.hpp"
#include "../game_config.hpp"
#include "../log.hpp"
#include "../map.hpp" // gamemap::MAX_PLAYERS
#include "../network.hpp"
#include "../filesystem.hpp"
#include "../multiplayer_error_codes.hpp"
#include "../serialization/parser.hpp"
#include "../serialization/preprocessor.hpp"
#include "../serialization/string_utils.hpp"
#include "../util.hpp"
#include "game.hpp"
#include "input_stream.hpp"
#include "metrics.hpp"
#include "player.hpp"
#include "proxy.hpp"
#include "simple_wml.hpp"
#include "ban.hpp"
#include "user_handler.hpp"
#include "sample_user_handler.hpp"
#ifdef HAVE_MYSQLPP
#include "forum_user_handler.hpp"
#endif
#include <boost/bind.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/scoped_array.hpp>
#include <boost/utility.hpp>
#include <algorithm>
#include <cassert>
#include <cerrno>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <map>
#include <set>
#include <sstream>
#include <vector>
#include <queue>
#include <csignal>
#ifndef _WIN32
#include <sys/times.h>
namespace {
clock_t get_cpu_time(bool active) {
if(!active) {
return 0;
}
struct tms buf;
times(&buf);
return buf.tms_utime + buf.tms_stime;
}
}
#else
// on Windows we don't calculate CPU time
clock_t get_cpu_time(bool /*active*/) {
return 0;
}
#endif
namespace {
bool match_user(std::pair<network::connection, wesnothd::player> pl, const std::string& username, const std::string& ip) {
return pl.second.name() == username && network::ip_address(pl.first) == ip;
}
}
static lg::log_domain log_server("server");
/**
* fatal and directly server related errors/warnings,
* ie not caused by erroneous client data
*/
#define ERR_SERVER LOG_STREAM(err, log_server)
/** clients send wrong/unexpected data */
#define WRN_SERVER LOG_STREAM(warn, log_server)
/** normal events */
#define LOG_SERVER LOG_STREAM(info, log_server)
#define DBG_SERVER LOG_STREAM(debug, log_server)
static lg::log_domain log_config("config");
#define ERR_CONFIG LOG_STREAM(err, log_config)
#define WRN_CONFIG LOG_STREAM(warn, log_config)
//compatibility code for MS compilers
#ifndef SIGHUP
#define SIGHUP 20
#endif
/** @todo FIXME: should define SIGINT here too, but to what? */
sig_atomic_t config_reload = 0;
#ifndef _MSC_VER
static void reload_config(int signal) {
assert(signal == SIGHUP);
config_reload = 1;
}
#endif
static void exit_sigint(int signal) {
assert(signal == SIGINT);
LOG_SERVER << "SIGINT caught, exiting without cleanup immediately.\n";
exit(128 + SIGINT);
}
static void exit_sigterm(int signal) {
assert(signal == SIGTERM);
LOG_SERVER << "SIGTERM caught, exiting without cleanup immediately.\n";
exit(128 + SIGTERM);
}
namespace {
// we take profiling info on every n requests
int request_sample_frequency = 1;
void send_doc(simple_wml::document& doc, network::connection connection, std::string type = "")
{
if (type.empty())
type = doc.root().first_child().to_string();
try {
simple_wml::string_span s = doc.output_compressed();
network::send_raw_data(s.begin(), s.size(), connection, type);
} catch (simple_wml::error& e) {
WRN_CONFIG << __func__ << ": simple_wml error: " << e.message << std::endl;
}
}
void make_add_diff(const simple_wml::node& src, const char* gamelist,
const char* type,
simple_wml::document& out, int index=-1)
{
if (!out.child("gamelist_diff")) {
out.root().add_child("gamelist_diff");
}
simple_wml::node* top = out.child("gamelist_diff");
if(gamelist) {
top = &top->add_child("change_child");
top->set_attr_int("index", 0);
top = &top->add_child("gamelist");
}
simple_wml::node& insert = top->add_child("insert_child");
const simple_wml::node::child_list& children = src.children(type);
assert(!children.empty());
if(index < 0) {
index = children.size() - 1;
}
assert(index < static_cast<int>(children.size()));
insert.set_attr_int("index", index);
children[index]->copy_into(insert.add_child(type));
}
bool make_delete_diff(const simple_wml::node& src,
const char* gamelist,
const char* type,
const simple_wml::node* remove,
simple_wml::document& out)
{
if (!out.child("gamelist_diff")) {
out.root().add_child("gamelist_diff");
}
simple_wml::node* top = out.child("gamelist_diff");
if(gamelist) {
top = &top->add_child("change_child");
top->set_attr_int("index", 0);
top = &top->add_child("gamelist");
}
const simple_wml::node::child_list& children = src.children(type);
const simple_wml::node::child_list::const_iterator itor =
std::find(children.begin(), children.end(), remove);
if(itor == children.end()) {
return false;
}
const int index = itor - children.begin();
simple_wml::node& del = top->add_child("delete_child");
del.set_attr_int("index", index);
del.add_child(type);
return true;
}
bool make_change_diff(const simple_wml::node& src,
const char* gamelist,
const char* type,
const simple_wml::node* item,
simple_wml::document& out)
{
if (!out.child("gamelist_diff")) {
out.root().add_child("gamelist_diff");
}
simple_wml::node* top = out.child("gamelist_diff");
if(gamelist) {
top = &top->add_child("change_child");
top->set_attr_int("index", 0);
top = &top->add_child("gamelist");
}
const simple_wml::node::child_list& children = src.children(type);
const simple_wml::node::child_list::const_iterator itor =
std::find(children.begin(), children.end(), item);
if(itor == children.end()) {
return false;
}
simple_wml::node& diff = *top;
simple_wml::node& del = diff.add_child("delete_child");
const int index = itor - children.begin();
del.set_attr_int("index", index);
del.add_child(type);
//inserts will be processed first by the client, so insert at index+1,
//and then when the delete is processed we'll slide into the right position
simple_wml::node& insert = diff.add_child("insert_child");
insert.set_attr_int("index", index + 1);
children[index]->copy_into(insert.add_child(type));
return true;
}
std::string player_status(wesnothd::player_map::const_iterator pl) {
std::ostringstream out;
const network::connection_stats& stats = network::get_connection_stats(pl->first);
const int time_connected = stats.time_connected / 1000;
const int seconds = time_connected % 60;
const int minutes = (time_connected / 60) % 60;
const int hours = time_connected / (60 * 60);
out << "'" << pl->second.name() << "' @ " << network::ip_address(pl->first)
<< " connected for " << std::setw(2) << hours << ":" << std::setw(2) << minutes << ":" << std::setw(2) << seconds
<< " sent " << stats.bytes_sent << " bytes, received "
<< stats.bytes_received << " bytes";
return out.str();
}
} // namespace
class fps_limiter {
size_t start_ticks_;
size_t ms_per_frame_;
public:
fps_limiter(size_t ms_per_frame = 20) : start_ticks_(0), ms_per_frame_(ms_per_frame)
{}
void limit() {
size_t current_ticks = SDL_GetTicks();
if (current_ticks - start_ticks_ < ms_per_frame_) {
SDL_Delay(ms_per_frame_ - (current_ticks - start_ticks_));
start_ticks_ += ms_per_frame_;
} else {
start_ticks_ = current_ticks;
}
}
void set_ms_per_frame(size_t ms_per_frame)
{
ms_per_frame_ = ms_per_frame;
}
void set_fps(size_t fps)
{
ms_per_frame_ = 1000 / fps;
}
};
static fps_limiter fps_limit_;
namespace {
const std::string denied_msg = "You're not allowed to execute this command.";
const std::string help_msg = "Available commands are: adminmsg <msg>,"
" ban <mask> <time> <reason>, bans [deleted] [<ipmask>], clones,"
" dul|deny_unregistered_login [yes|no], kick <mask> [<reason>],"
" k[ick]ban <mask> <time> <reason>, help, games, metrics,"
" netstats [all], [lobby]msg <message>, motd [<message>],"
" pm|privatemsg <nick> <message>, requests, sample, searchlog <mask>,"
" signout, stats, status [<mask>], unban <ipmask>\n"
"Specific strings (those not inbetween <> like the command names)"
" are case insensitive.";
}
server::server(int port, const std::string& config_file, size_t min_threads,
size_t max_threads) :
net_manager_(min_threads, max_threads),
server_(port),
ban_manager_(),
ip_log_(),
user_handler_(NULL),
seeds_(),
players_(),
ghost_players_(),
games_(),
not_logged_in_(),
rooms_(players_),
input_(),
config_file_(config_file),
cfg_(read_config()),
accepted_versions_(),
redirected_versions_(),
proxy_versions_(),
disallowed_names_(),
admin_passwd_(),
admins_(),
motd_(),
default_max_messages_(0),
default_time_period_(0),
concurrent_connections_(0),
graceful_restart(false),
lan_server_(time(NULL)),
last_user_seen_time_(time(NULL)),
restart_command(),
max_ip_log_size_(0),
uh_name_(),
deny_unregistered_login_(false),
save_replays_(false),
replay_save_path_(),
allow_remote_shutdown_(false),
tor_ip_list_(),
version_query_response_("[version]\n[/version]\n", simple_wml::INIT_COMPRESSED),
login_response_("[mustlogin]\n[/mustlogin]\n", simple_wml::INIT_COMPRESSED),
join_lobby_response_("[join_lobby]\n[/join_lobby]\n", simple_wml::INIT_COMPRESSED),
games_and_users_list_("[gamelist]\n[/gamelist]\n", simple_wml::INIT_STATIC),
metrics_(),
last_ping_(time(NULL)),
last_stats_(last_ping_),
last_uh_clean_(last_ping_),
cmd_handlers_()
{
setup_handlers();
load_config();
ban_manager_.read();
rooms_.read_rooms();
#ifndef _MSC_VER
signal(SIGHUP, reload_config);
#endif
signal(SIGINT, exit_sigint);
signal(SIGTERM, exit_sigterm);
}
void server::setup_handlers()
{
cmd_handlers_["shut_down"] = &server::shut_down_handler;
cmd_handlers_["restart"] = &server::restart_handler;
cmd_handlers_["sample"] = &server::sample_handler;
cmd_handlers_["help"] = &server::help_handler;
cmd_handlers_["stats"] = &server::stats_handler;
cmd_handlers_["metrics"] = &server::metrics_handler;
cmd_handlers_["requests"] = &server::requests_handler;
cmd_handlers_["games"] = &server::games_handler;
cmd_handlers_["wml"] = &server::wml_handler;
cmd_handlers_["netstats"] = &server::netstats_handler;
cmd_handlers_["report"] = &server::adminmsg_handler;
cmd_handlers_["adminmsg"] = &server::adminmsg_handler;
cmd_handlers_["pm"] = &server::pm_handler;
cmd_handlers_["privatemsg"] = &server::pm_handler;
cmd_handlers_["msg"] = &server::msg_handler;
cmd_handlers_["lobbymsg"] = &server::msg_handler;
cmd_handlers_["status"] = &server::status_handler;
cmd_handlers_["clones"] = &server::clones_handler;
cmd_handlers_["bans"] = &server::bans_handler;
cmd_handlers_["ban"] = &server::ban_handler;
cmd_handlers_["unban"] = &server::unban_handler;
cmd_handlers_["ungban"] = &server::ungban_handler;
cmd_handlers_["kick"] = &server::kick_handler;
cmd_handlers_["kickban"] = &server::kickban_handler;
cmd_handlers_["kban"] = &server::kickban_handler;
cmd_handlers_["gban"] = &server::gban_handler;
cmd_handlers_["motd"] = &server::motd_handler;
cmd_handlers_["searchlog"] = &server::searchlog_handler;
cmd_handlers_["sl"] = &server::searchlog_handler;
cmd_handlers_["dul"] = &server::dul_handler;
cmd_handlers_["deny_unregistered_login"] = &server::dul_handler;
}
void server::send_error(network::connection sock, const char* msg, const char* error_code) const
{
simple_wml::document doc;
doc.root().add_child("error").set_attr("message", msg);
if(*error_code != '\0') {
doc.child("error")->set_attr("error_code", error_code);
}
send_doc(doc, sock, "error");
}
void server::send_warning(network::connection sock, const char* msg, const char* warning_code) const
{
simple_wml::document doc;
doc.root().add_child("warning").set_attr("message", msg);
if(*warning_code != '\0') {
doc.child("warning")->set_attr("warning_code", warning_code);
}
send_doc(doc, sock, "warning");
}
void server::send_password_request(network::connection sock, const std::string& msg,
const std::string& user, const char* error_code, bool force_confirmation)
{
std::string salt = user_handler_->create_salt();
std::string pepper = user_handler_->create_pepper(user);
std::string spices = pepper + salt;
if(user_handler_->use_phpbb_encryption() && pepper.empty()) {
send_error(sock, "Even though your nick is registered on this server you "
"cannot log in due to an error in the hashing algorithm. "
"Logging into your forum account on http://forum.wesnoth.org "
"may fix this problem.");
return;
}
seeds_.insert(std::pair<network::connection, std::string>(sock, salt));
simple_wml::document doc;
simple_wml::node& e = doc.root().add_child("error");
e.set_attr("message", msg.c_str());
e.set_attr("password_request", "yes");
e.set_attr("phpbb_encryption", user_handler_->use_phpbb_encryption() ? "yes" : "no");
e.set_attr("salt", spices.c_str());
e.set_attr("force_confirmation", force_confirmation ? "yes" : "no");
if(*error_code != '\0') {
e.set_attr("error_code", error_code);
}
send_doc(doc, sock, "error");
}
config server::read_config() const {
config configuration;
if (config_file_ == "") return configuration;
try {
scoped_istream stream = preprocess_file(config_file_);
read(configuration, *stream);
LOG_SERVER << "Server configuration from file: '" << config_file_
<< "' read.\n";
} catch(config::error& e) {
ERR_CONFIG << "ERROR: Could not read configuration file: '"
<< config_file_ << "': '" << e.message << "'.\n";
}
return configuration;
}
void server::load_config() {
#ifndef FIFODIR
# ifdef _MSC_VER
# pragma message ("No FIFODIR set")
# define FIFODIR "d:/"
# else
# warning "No FIFODIR set"
# ifdef _WIN32
# define FIFODIR "d:/"
# else
# define FIFODIR "/var/run/wesnothd"
# endif
# endif
#endif
const std::string fifo_path = (cfg_["fifo_path"].empty() ? std::string(FIFODIR) + "/socket" : std::string(cfg_["fifo_path"]));
input_.reset();
input_.reset(new input_stream(fifo_path));
save_replays_ = cfg_["save_replays"].to_bool();
replay_save_path_ = cfg_["replay_save_path"].str();
tor_ip_list_ = utils::split(cfg_["tor_ip_list_path"].empty() ? "" : read_file(cfg_["tor_ip_list_path"]), '\n');
admin_passwd_ = cfg_["passwd"].str();
motd_ = cfg_["motd"].str();
lan_server_ = lexical_cast_default<time_t>(cfg_["lan_server"], 0);
uh_name_ = cfg_["user_handler"].str();
deny_unregistered_login_ = cfg_["deny_unregistered_login"].to_bool();
allow_remote_shutdown_ = cfg_["allow_remote_shutdown"].to_bool();
disallowed_names_.clear();
if (cfg_["disallow_names"] == "") {
disallowed_names_.push_back("*admin*");
disallowed_names_.push_back("*admln*");
disallowed_names_.push_back("*server*");
disallowed_names_.push_back("player");
disallowed_names_.push_back("network");
disallowed_names_.push_back("human");
disallowed_names_.push_back("computer");
disallowed_names_.push_back("ai");
disallowed_names_.push_back("ai?");
} else {
disallowed_names_ = utils::split(cfg_["disallow_names"]);
}
default_max_messages_ = cfg_["max_messages"].to_int(4);
default_time_period_ = cfg_["messages_time_period"].to_int(10);
concurrent_connections_ = cfg_["connections_allowed"].to_int(5);
max_ip_log_size_ = cfg_["max_ip_log_size"].to_int(500);
// Example config line:
// restart_command="./wesnothd-debug -d -c ~/.wesnoth1.5/server.cfg"
// remember to make new one as a daemon or it will block old one
restart_command = cfg_["restart_command"].str();
fps_limit_.set_ms_per_frame(cfg_["ms_per_frame"].to_int(20));
accepted_versions_.clear();
const std::string& versions = cfg_["versions_accepted"];
if (versions.empty() == false) {
accepted_versions_ = utils::split(versions);
} else {
accepted_versions_.push_back(game_config::version);
accepted_versions_.push_back("test");
}
redirected_versions_.clear();
foreach (const config &redirect, cfg_.child_range("redirect")) {
foreach (const std::string &version, utils::split(redirect["version"])) {
redirected_versions_[version] = redirect;
}
}
proxy_versions_.clear();
foreach (const config &proxy, cfg_.child_range("proxy")) {
foreach (const std::string &version, utils::split(proxy["version"])) {
proxy_versions_[version] = proxy;
}
}
ban_manager_.load_config(cfg_);
rooms_.load_config(cfg_);
// If there is a [user_handler] tag in the config file
// allow nick registration, otherwise we set user_handler_
// to NULL. Thus we must check user_handler_ for not being
// NULL everytime we want to use it.
user_handler_.reset();
if (const config &user_handler = cfg_.child("user_handler")) {
if(uh_name_ == "sample") {
user_handler_.reset(new suh(user_handler));
}
#ifdef HAVE_MYSQLPP
else if(uh_name_ == "forum" || uh_name_.empty()) {
user_handler_.reset(new fuh(user_handler));
}
#endif
// Initiate the mailer class with the [mail] tag
// from the config file
if (user_handler_) user_handler_->init_mailer(cfg_.child("mail"));
}
}
bool server::ip_exceeds_connection_limit(const std::string& ip) const {
if (concurrent_connections_ == 0) return false;
size_t connections = 0;
for (wesnothd::player_map::const_iterator i = players_.begin(); i != players_.end(); ++i) {
if (network::ip_address(i->first) == ip) {
++connections;
}
}
return connections >= concurrent_connections_;
}
std::string server::is_ip_banned(const std::string& ip) const {
if (!tor_ip_list_.empty()) {
if (find(tor_ip_list_.begin(), tor_ip_list_.end(), ip) != tor_ip_list_.end()) return "TOR IP";
}
return ban_manager_.is_ip_banned(ip);
}
void server::dump_stats(const time_t& now) {
last_stats_ = now;
LOG_SERVER << "Statistics:"
<< "\tnumber_of_games = " << games_.size()
<< "\tnumber_of_users = " << players_.size()
<< "\tlobby_users = " << rooms_.lobby().size() << "\n";
}
void server::clean_user_handler(const time_t& now) {
if(!user_handler_) {
return;
}
last_uh_clean_ = now;
user_handler_->clean_up();
}
void server::run() {
int graceful_counter = 0;
for (int loop = 0;; ++loop) {
// Try to run with 50 FPS all the time
// Server will respond a bit faster under heavy load
fps_limit_.limit();
try {
// We are going to waith 10 seconds before shutting down so users can get out of game.
if (graceful_restart && games_.empty() && ++graceful_counter > 500 )
{
// TODO: We should implement client side autoreconnect.
// Idea:
// server should send [reconnect]host=host,port=number[/reconnect]
// Then client would reconnect to new server automatically.
// This would also allow server to move to new port or address if there is need
process_command("msg All games ended. Shutting down now. Reconnect to the new server instance.", "system");
throw network::error("shut down");
}
if (config_reload == 1) {
cfg_ = read_config();
load_config();
config_reload = 0;
}
// Process commands from the server socket/fifo
std::string admin_cmd;
if (input_ && input_->read_line(admin_cmd)) {
LOG_SERVER << "Admin Command: type: " << admin_cmd << "\n";
const std::string res = process_command(admin_cmd, "*socket*");
// Only mark the response if we fake the issuer (i.e. command comes from IRC or so)
if (admin_cmd.at(0) == '+') {
LOG_SERVER << "[admin_command_response]\n" << res << "\n" << "[/admin_command_response]\n";
} else {
LOG_SERVER << res << "\n";
}
}
time_t now = time(NULL);
if (last_ping_ + network::ping_interval <= now) {
if (lan_server_ && players_.empty() && last_user_seen_time_ + lan_server_ < now)
{
LOG_SERVER << "Lan server has been empty for " << (now - last_user_seen_time_) << " seconds. Shutting down!\n";
// We have to shutdown
graceful_restart = true;
}
// and check if bans have expired
ban_manager_.check_ban_times(now);
// Make sure we log stats every 5 minutes
if (last_stats_ + 5 * 60 <= now) {
dump_stats(now);
if (rooms_.dirty()) rooms_.write_rooms();
}
// Cleaning the user_handler once a day should be more than enough
if (last_uh_clean_ + 60 * 60 * 24 <= now) {
clean_user_handler(now);
}
// Send network stats every hour
static int prev_hour = localtime(&now)->tm_hour;
if (prev_hour != localtime(&now)->tm_hour)
{
prev_hour = localtime(&now)->tm_hour;
LOG_SERVER << network::get_bandwidth_stats();
}
// send a 'ping' to all players to detect ghosts
DBG_SERVER << "Pinging inactive players.\n" ;
std::ostringstream strstr ;
strstr << "ping=\"" << now << "\"" ;
simple_wml::document ping( strstr.str().c_str(),
simple_wml::INIT_COMPRESSED );
simple_wml::string_span s = ping.output_compressed();
foreach (network::connection sock, ghost_players_) {
if (!lg::debug.dont_log(log_server)) {
wesnothd::player_map::const_iterator i = players_.find(sock);
if (i != players_.end()) {
DBG_SERVER << "Pinging " << i->second.name() << "(" << i->first << ").\n";
} else {
ERR_SERVER << "Player " << sock << " is in ghost_players_ but not in players_.\n";
}
}
network::send_raw_data(s.begin(), s.size(), sock, "ping") ;
}
// Copy new player list on top of ghost_players_ list.
// Only a single thread should be accessing this
// Erase before we copy - speeds inserts
ghost_players_.clear();
foreach (const wesnothd::player_map::value_type v, players_) {
ghost_players_.insert(v.first);
}
last_ping_ = now;
}
network::process_send_queue();
network::connection sock = network::accept_connection();
if (sock) {
const std::string ip = network::ip_address(sock);
const std::string reason = is_ip_banned(ip);
if (!reason.empty()) {
LOG_SERVER << ip << "\trejected banned user. Reason: " << reason << "\n";
send_error(sock, "You are banned. Reason: " + reason);
network::disconnect(sock);
} else if (ip_exceeds_connection_limit(ip)) {
LOG_SERVER << ip << "\trejected ip due to excessive connections\n";
send_error(sock, "Too many connections from your IP.");
network::disconnect(sock);
} else {
DBG_SERVER << ip << "\tnew connection accepted. (socket: "
<< sock << ")\n";
send_doc(version_query_response_, sock);
not_logged_in_.insert(sock);
}
}
static int sample_counter = 0;
std::vector<char> buf;
network::bandwidth_in_ptr bandwidth_type;
while ((sock = network::receive_data(buf, &bandwidth_type)) != network::null_connection) {
metrics_.service_request();
if(buf.empty()) {
WRN_SERVER << "received empty packet\n";
continue;
}
const bool sample = request_sample_frequency >= 1 && (sample_counter++ % request_sample_frequency) == 0;
const clock_t before_parsing = get_cpu_time(sample);
char* buf_ptr = new char [buf.size()];
memcpy(buf_ptr, &buf[0], buf.size());
simple_wml::string_span compressed_buf(buf_ptr, buf.size());
boost::scoped_ptr<simple_wml::document> data_ptr;
try {
data_ptr.reset(new simple_wml::document(compressed_buf)); // might throw a simple_wml::error
data_ptr->take_ownership_of_buffer(buf_ptr);
} catch (simple_wml::error& e) {
WRN_CONFIG << "simple_wml error in received data: " << e.message << std::endl;
send_error(sock, "Invalid WML received: " + e.message);
delete [] buf_ptr;
continue;
} catch(...) {
delete [] buf_ptr;
throw;
}
simple_wml::document& data = *data_ptr;
std::vector<char>().swap(buf);
const clock_t after_parsing = get_cpu_time(sample);
process_data(sock, data);
bandwidth_type->set_type(data.root().first_child().to_string());
if(sample) {
const clock_t after_processing = get_cpu_time(sample);
metrics_.record_sample(data.root().first_child(),
after_parsing - before_parsing,
after_processing - after_parsing);
}
}
metrics_.no_requests();
} catch(simple_wml::error& e) {
WRN_CONFIG << "Warning: error in received data: " << e.message << "\n";
} catch(network::error& e) {
if (e.message == "shut down") {
LOG_SERVER << "Try to disconnect all users...\n";
for (wesnothd::player_map::const_iterator pl = players_.begin();
pl != players_.end(); ++pl)
{
network::disconnect(pl->first);
}
LOG_SERVER << "Shutting server down.\n";
break;
}
if (!e.socket) {
ERR_SERVER << "network error: " << e.message << "\n";
e.disconnect();
continue;
}
DBG_SERVER << "socket closed: " << e.message << "\n";
const std::string ip = network::ip_address(e.socket);
if (proxy::is_proxy(e.socket)) {
LOG_SERVER << ip << "\tProxy user disconnected.\n";
proxy::disconnect(e.socket);
e.disconnect();
DBG_SERVER << "done closing socket...\n";
continue;
}
// Was the user already logged in?
const wesnothd::player_map::iterator pl_it = players_.find(e.socket);
if (pl_it == players_.end()) {
std::set<network::connection>::iterator i = not_logged_in_.find(e.socket);
if (i != not_logged_in_.end()) {
DBG_SERVER << ip << "\tNot logged in user disconnected.\n";
not_logged_in_.erase(i);
} else {
WRN_SERVER << ip << "\tWarning: User disconnected right after the connection was accepted.\n";
}
e.disconnect();
DBG_SERVER << "done closing socket...\n";
continue;
}
const simple_wml::node::child_list& users = games_and_users_list_.root().children("user");
const size_t index = std::find(users.begin(), users.end(), pl_it->second.config_address()) - users.begin();
if (index < users.size()) {
simple_wml::document diff;
if(make_delete_diff(games_and_users_list_.root(), NULL, "user",
pl_it->second.config_address(), diff)) {
rooms_.lobby().send_data(diff, e.socket);
}
games_and_users_list_.root().remove_child("user", index);
} else {
ERR_SERVER << ip << "ERROR: Could not find user to remove: "
<< pl_it->second.name() << " in games_and_users_list_.\n";
}
// Was the player in the lobby or a game?
if (rooms_.in_lobby(e.socket)) {
rooms_.remove_player(e.socket);
LOG_SERVER << ip << "\t" << pl_it->second.name()
<< "\thas logged off. (socket: " << e.socket << ")\n";
} else {
for (std::vector<wesnothd::game*>::iterator g = games_.begin();
g != games_.end(); ++g)
{
if (!(*g)->is_member(e.socket)) {
continue;
}
// Did the last player leave?
if ((*g)->remove_player(e.socket, true)) {
delete_game(g);
break;
} else {
(*g)->describe_slots();
update_game_in_lobby(*g, e.socket);
}
break;
}
}
// Find the matching nick-ip pair in the log and update the sign off time
connection_log ip_name = connection_log(pl_it->second.name(), ip, 0);
std::deque<connection_log>::iterator i = std::find(ip_log_.begin(), ip_log_.end(), ip_name);
if(i != ip_log_.end()) {
i->log_off = time(NULL);
}
players_.erase(pl_it);
ghost_players_.erase(e.socket);
if (lan_server_)
{
last_user_seen_time_ = time(0);
}
e.disconnect();
DBG_SERVER << "done closing socket...\n";
// Catch user_handler exceptions here, to prevent the
// server from going down completely. Once we are sure
// all user_handler exceptions are caught correctly
// this can removed.
} catch (user_handler::error& e) {
ERR_SERVER << "Uncaught user_handler exception: " << e.message << "\n";
}
}
}
void server::process_data(const network::connection sock,
simple_wml::document& data) {
if (proxy::is_proxy(sock)) {
proxy::received_data(sock, data);
return;
}
// We know the client is alive for this interval
// Remove player from ghost_players map if selective_ping
// is enabled for the player.
if (ghost_players_.find(sock) != ghost_players_.end()) {
const wesnothd::player_map::const_iterator pl = players_.find(sock);
if (pl != players_.end()) {
if (pl->second.selective_ping() ) {
ghost_players_.erase(sock);
}
}
}
// Process the message
simple_wml::node& root = data.root();
if(root.has_attr("ping")) {
// Ignore client side pings for now.
return;
} else if(not_logged_in_.find(sock) != not_logged_in_.end()) {
// Someone who is not yet logged in is sending login details.
process_login(sock, data);
} else if (simple_wml::node* query = root.child("query")) {
process_query(sock, *query);
} else if (simple_wml::node* nickserv = root.child("nickserv")) {
process_nickserv(sock, *nickserv);
} else if (simple_wml::node* whisper = root.child("whisper")) {
process_whisper(sock, *whisper);
} else if (rooms_.in_lobby(sock)) {
process_data_lobby(sock, data);
} else {
process_data_game(sock, data);
}
}
void server::process_login(const network::connection sock,
simple_wml::document& data) {
// See if the client is sending their version number.
if (const simple_wml::node* const version = data.child("version")) {
const simple_wml::string_span& version_str_span = (*version)["version"];
const std::string version_str(version_str_span.begin(),
version_str_span.end());
std::vector<std::string>::const_iterator accepted_it;
// Check if it is an accepted version.
for (accepted_it = accepted_versions_.begin();
accepted_it != accepted_versions_.end(); ++accepted_it) {
if (utils::wildcard_string_match(version_str, *accepted_it)) break;
}
if (accepted_it != accepted_versions_.end()) {
LOG_SERVER << network::ip_address(sock)
<< "\tplayer joined using accepted version " << version_str
<< ":\ttelling them to log in.\n";
send_doc(login_response_, sock);
return;
}
std::map<std::string, config>::const_iterator config_it;
// Check if it is a redirected version
for (config_it = redirected_versions_.begin();
config_it != redirected_versions_.end(); ++config_it)
{
if (utils::wildcard_string_match(version_str, config_it->first))
break;
}
if (config_it != redirected_versions_.end()) {
LOG_SERVER << network::ip_address(sock)
<< "\tplayer joined using version " << version_str
<< ":\tredirecting them to " << config_it->second["host"]
<< ":" << config_it->second["port"] << "\n";
config response;
response.add_child("redirect", config_it->second);
network::send_data(response, sock, "redirect");
return;
}
// Check if it's a version we should start a proxy for.
for (config_it = proxy_versions_.begin();
config_it != proxy_versions_.end(); ++config_it)
{
if (utils::wildcard_string_match(version_str, config_it->first))
break;
}
if (config_it != proxy_versions_.end()) {
LOG_SERVER << network::ip_address(sock)
<< "\tplayer joined using version " << version_str
<< ":\tconnecting them by proxy to " << config_it->second["host"]
<< ":" << config_it->second["port"] << "\n";
proxy::create_proxy(sock, config_it->second["host"],
config_it->second["port"].to_int(15000));
return;
}
// No match, send a response and reject them.
LOG_SERVER << network::ip_address(sock)
<< "\tplayer joined using unknown version " << version_str
<< ":\trejecting them\n";
config response;
if (!accepted_versions_.empty()) {
response["version"] = *accepted_versions_.begin();
} else if (redirected_versions_.empty() == false) {
response["version"] = redirected_versions_.begin()->first;
} else {
ERR_SERVER << "ERROR: This server doesn't accept any versions at all.\n";
response["version"] = "null";
}
network::send_data(response, sock, "error");
return;
}
const simple_wml::node* const login = data.child("login");
// Client must send a login first.
if (login == NULL) {
send_error(sock, "You must login first.", MP_MUST_LOGIN);
return;
}
// Check if the username is valid (all alpha-numeric plus underscore and hyphen)
std::string username = (*login)["username"].to_string();
if (!utils::isvalid_username(username)) {
send_error(sock, "The nick '" + username + "' contains invalid "
"characters. Only alpha-numeric characters, underscores and hyphens"
"are allowed.", MP_INVALID_CHARS_IN_NAME_ERROR);
return;
}
if (username.size() > 20) {
send_error(sock, "The nick '" + username + "' is too long. Nicks must be 20 characters or less.",
MP_NAME_TOO_LONG_ERROR);
return;
}
// Check if the username is allowed.
for (std::vector<std::string>::const_iterator d_it = disallowed_names_.begin();
d_it != disallowed_names_.end(); ++d_it)
{
if (utils::wildcard_string_match(utils::lowercase(username),
utils::lowercase(*d_it)))
{
send_error(sock, "The nick '" + username + "' is reserved and cannot be used by players",
MP_NAME_RESERVED_ERROR);
return;
}
}
// If this is a request for password reminder
if(user_handler_) {
std::string password_reminder = (*login)["password_reminder"].to_string();
if(password_reminder == "yes") {
try {
user_handler_->password_reminder(username);
send_error(sock, "Your password reminder email has been sent.");
} catch (user_handler::error& e) {
send_error(sock, "There was an error sending your password reminder email. The error message was: " +
e.message);
}
return;
}
}
// Check the username isn't already taken
wesnothd::player_map::const_iterator p;
for (p = players_.begin(); p != players_.end(); ++p) {
if (p->second.name() == username) {
break;
}
}
// Check for password
// Current login procedure for registered nicks is:
// - Client asks to log in with a particular nick
// - Server sends client random salt plus some info
// generated from the original hash that is required to
// regenerate the hash
// - Client generates hash for the user provided password
// and mixes it with the received random salt
// - Server received salted hash, salts the valid hash with
// the same salt it sent to the client and compares the results
bool registered = false;
if(user_handler_) {
std::string password = (*login)["password"].to_string();
const bool exists = user_handler_->user_exists(username);
// This name is registered but the account is not active
if(exists && !user_handler_->user_is_active(username)) {
send_warning(sock, "The nick '" + username + "' is inactive. You cannot claim ownership of this "
"nick until you activate your account via email or ask an administrator to do it for you.", MP_NAME_INACTIVE_WARNING);
//registered = false;
}
else if(exists) {
// This name is registered and no password provided
if(password.empty()) {
if(p == players_.end()) {
send_password_request(sock, "The nick '" + username +"' is registered on this server.",
username, MP_PASSWORD_REQUEST);
} else {
send_password_request(sock, "The nick '" + username + "' is registered on this server."
"\n\nWARNING: There is already a client using this username, "
"logging in will cause that client to be kicked!",
username, MP_PASSWORD_REQUEST_FOR_LOGGED_IN_NAME, true);
}
return;
}
// A password (or hashed password) was provided, however
// there is no seed
if(seeds_[sock].empty()) {
send_password_request(sock, "Please try again.", username, MP_NO_SEED_ERROR);
}
// This name is registered and an incorrect password provided
else if(!(user_handler_->login(username, password, seeds_[sock]))) {
// Reset the random seed
seeds_.erase(sock);
send_password_request(sock, "The password you provided for the nick '" + username +
"' was incorrect.", username, MP_INCORRECT_PASSWORD_ERROR);
LOG_SERVER << network::ip_address(sock) << "\t"
<< "Login attempt with incorrect password for nick '" << username << "'.\n";
return;
}
// This name exists and the password was neither empty nor incorrect
registered = true;
// Reset the random seed
seeds_.erase(sock);
user_handler_->user_logged_in(username);
}
}
// If we disallow unregistered users and this user is not registered send an error
if(user_handler_ && !registered && deny_unregistered_login_) {
send_error(sock, "The nick '" + username + "' is not registered. "
"This server disallows unregistered nicks.", MP_NAME_UNREGISTERED_ERROR);
return;
}
if(p != players_.end()) {
if(registered) {
// If there is already a client using this username kick it
process_command("kick " + p->second.name() + " autokick by registered user", username);
} else {
send_error(sock, "The nick '" + username + "' is already taken.", MP_NAME_TAKEN_ERROR);
return;
}
}
// Check if the version is now available. If it is not, this player must
// always be pinged.
bool selective_ping = false ;
if( (*login)["selective_ping"].to_bool() ) {
selective_ping = true ;
DBG_SERVER << "selective ping is ENABLED for " << sock << "\n" ;
} else {
DBG_SERVER << "selective ping is DISABLED for " << sock << "\n" ;
}
send_doc(join_lobby_response_, sock);
simple_wml::node& player_cfg = games_and_users_list_.root().add_child("user");
const wesnothd::player new_player(username, player_cfg, registered,
default_max_messages_, default_time_period_, selective_ping,
user_handler_ && user_handler_->user_is_moderator(username));
// If the new player does not have selective ping enabled, immediately
// add the player to the ghost player's list. This ensures a client won't
// have to wait as long as x2 the current ping delay; which could cause
// a client-side disconnection.
players_.insert(std::make_pair(sock, new_player));
if( !selective_ping )
ghost_players_.insert(sock) ;
not_logged_in_.erase(sock);
rooms_.enter_lobby(sock);
// Send the new player the entire list of games and players
send_doc(games_and_users_list_, sock);
if (motd_ != "") {
rooms_.lobby().send_server_message(motd_, sock);
}
// Send other players in the lobby the update that the player has joined
simple_wml::document diff;
make_add_diff(games_and_users_list_.root(), NULL, "user", diff);
rooms_.lobby().send_data(diff, sock);
LOG_SERVER << network::ip_address(sock) << "\t" << username
<< "\thas logged on" << (registered ? " to a registered account" : "")
<< ". (socket: " << sock << ")\n";
for (std::vector<wesnothd::game*>::const_iterator g = games_.begin(); g != games_.end(); ++g) {
// Note: This string is parsed by the client to identify lobby join messages!
(*g)->send_server_message_to_all(username + " has logged into the lobby");
}
if(user_handler_ && user_handler_->user_is_moderator(username)) {
LOG_SERVER << "Admin automatically recognized: IP: "
<< network::ip_address(sock) << "\tnick: "
<< username << std::endl;
// This string is parsed by the client!
rooms_.lobby().send_server_message("You are now recognized as an administrator. "
"If you no longer want to be automatically authenticated use '/query signout'.", sock);
}
// Log the IP
connection_log ip_name = connection_log(username, network::ip_address(sock), 0);
if (std::find(ip_log_.begin(), ip_log_.end(), ip_name) == ip_log_.end()) {
ip_log_.push_back(ip_name);
// Remove the oldest entry if the size of the IP log exceeds the maximum size
if(ip_log_.size() > max_ip_log_size_) ip_log_.pop_front();
}
}
void server::process_query(const network::connection sock,
simple_wml::node& query) {
const wesnothd::player_map::iterator pl = players_.find(sock);
if (pl == players_.end()) {
DBG_SERVER << "ERROR: process_query(): Could not find player with socket: " << sock << "\n";
return;
}
const std::string command(query["type"].to_string());
std::ostringstream response;
const std::string& help_msg = "Available commands are: adminmsg <msg>, help, games, metrics,"
" motd, netstats [all], requests, sample, stats, status, wml.";
// Commands a player may issue.
if (command == "status") {
response << process_command(command + " " + pl->second.name(), pl->second.name());
} else if (command.find("adminmsg") == 0
|| command == "games"
|| command == "metrics"
|| command == "motd"
|| command == "netstats"
|| command == "netstats all"
|| command == "requests"
|| command == "sample"
|| command == "stats"
|| command == "status " + pl->second.name()
|| command == "wml")
{
response << process_command(command, pl->second.name());
} else if (pl->second.is_moderator()) {
if (command == "signout") {
LOG_SERVER << "Admin signed out: IP: "
<< network::ip_address(sock) << "\tnick: "
<< pl->second.name() << std::endl;
pl->second.set_moderator(false);
// This string is parsed by the client!
response << "You are no longer recognized as an administrator.";
if(user_handler_) {
user_handler_->set_is_moderator(pl->second.name(), false);
}
} else {
LOG_SERVER << "Admin Command: type: " << command
<< "\tIP: "<< network::ip_address(sock)
<< "\tnick: "<< pl->second.name() << std::endl;
response << process_command(command, pl->second.name());
LOG_SERVER << response.str() << std::endl;
}
} else if (command == "help" || command.empty()) {
response << help_msg;
} else if (command == "admin" || command.find("admin ") == 0) {
if (admin_passwd_.empty()) {
rooms_.lobby().send_server_message("No password set.", sock);
return;
}
std::string passwd;
if (command.size() >= 6) passwd = command.substr(6);
if (passwd == admin_passwd_) {
LOG_SERVER << "New Admin recognized: IP: "
<< network::ip_address(sock) << "\tnick: "
<< pl->second.name() << std::endl;
pl->second.set_moderator(true);
// This string is parsed by the client!
response << "You are now recognized as an administrator.";
if (user_handler_) {
user_handler_->set_is_moderator(pl->second.name(), true);
}
} else {
WRN_SERVER << "FAILED Admin attempt with password: '" << passwd << "'\tIP: "
<< network::ip_address(sock) << "\tnick: "
<< pl->second.name() << std::endl;
response << "Error: wrong password";
}
} else {
response << "Error: unrecognized query: '" << command << "'\n" << help_msg;
}
rooms_.lobby().send_server_message(response.str(), sock);
}
void server::start_new_server() {
if (restart_command.empty())
return;
// Example config line:
// restart_command="./wesnothd-debug -d -c ~/.wesnoth1.5/server.cfg"
// remember to make new one as a daemon or it will block old one
if (std::system(restart_command.c_str())) {
ERR_SERVER << "Failed to start new server with command: " << restart_command << "\n";
} else {
LOG_SERVER << "New server started with command: " << restart_command << "\n";
}
}
std::string server::process_command(std::string query, std::string issuer_name) {
utils::strip(query);
if (issuer_name == "*socket*" && query.at(0) == '+') {
// The first argument might be "+<issuer>: ".
// In that case we use +<issuer>+ as the issuer_name.
// (Mostly used for communication with IRC.)
std::string::iterator issuer_end =
std::find(query.begin(), query.end(), ':');
std::string issuer(query.begin() + 1, issuer_end);
if (!issuer.empty()) {
issuer_name = "+" + issuer + "+";
query = std::string(issuer_end + 1, query.end());
utils::strip(query);
}
}
const std::string::iterator i = std::find(query.begin(), query.end(), ' ');
const std::string command = utils::lowercase(std::string(query.begin(), i));
std::string parameters = (i == query.end() ? "" : std::string(i + 1, query.end()));
utils::strip(parameters);
std::ostringstream out;
std::map<std::string, server::cmd_handler>::iterator handler_itor = cmd_handlers_.find(command);
if(handler_itor == cmd_handlers_.end()) {
out << "Command '" << command << "' is not recognized.\n" << help_msg;
} else {
const cmd_handler &handler = handler_itor->second;
handler(this, issuer_name, query, parameters, &out);
}
return out.str();
}
// Shutdown, restart and sample commands can only be issued via the socket.
void server::shut_down_handler(const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (issuer_name != "*socket*" && !allow_remote_shutdown_) {
*out << denied_msg;
return;
}
if (parameters == "now") {
throw network::error("shut down");
} else {
// Graceful shut down.
server_.stop();
input_.reset();
graceful_restart = true;
process_command("msg The server is shutting down. You may finish your games but can't start new ones. Once all games have ended the server will exit.", issuer_name);
*out << "Server is doing graceful shut down.";
}
}
void server::restart_handler(const std::string& issuer_name, const std::string& /*query*/, std::string& /*parameters*/, std::ostringstream *out) {
assert(out != NULL);
if (issuer_name != "*socket*" && !allow_remote_shutdown_) {
*out << denied_msg;
return;
}
if (restart_command.empty()) {
*out << "No restart_command configured! Not restarting.";
} else {
graceful_restart = true;
// stop listening socket
server_.stop();
input_.reset();
// start new server
start_new_server();
process_command("msg The server has been restarted. You may finish current games but can't start new ones and new players can't join this (old) server instance. (So if a player of your game disconnects you have to save, reconnect and reload the game on the new server instance. It is actually recommended to do that right away.)", issuer_name);
*out << "New server started.";
}
}
void server::sample_handler(const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters.empty()) {
*out << "Current sample frequency: " << request_sample_frequency;
return;
} else if (issuer_name != "*socket*") {
*out << denied_msg;
return;
}
request_sample_frequency = atoi(parameters.c_str());
if (request_sample_frequency <= 0) {
*out << "Sampling turned off.";
} else {
*out << "Sampling every " << request_sample_frequency << " requests.";
}
}
void server::help_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& /*parameters*/, std::ostringstream *out) {
assert(out != NULL);
*out << help_msg;
}
void server::stats_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& /*parameters*/, std::ostringstream *out) {
assert(out != NULL);
*out << "Number of games = " << games_.size()
<< "\nTotal number of users = " << players_.size()
<< "\nNumber of users in the lobby = " << rooms_.lobby().size();
}
void server::metrics_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& /*parameters*/, std::ostringstream *out) {
assert(out != NULL);
*out << metrics_;
}
void server::requests_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& /*parameters*/, std::ostringstream *out) {
assert(out != NULL);
metrics_.requests(*out);
}
void server::games_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& /*parameters*/, std::ostringstream *out) {
assert(out != NULL);
metrics_.games(*out);
}
void server::wml_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& /*parameters*/, std::ostringstream *out) {
assert(out != NULL);
*out << simple_wml::document::stats();
}
void server::netstats_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
network::pending_statistics stats = network::get_pending_stats();
*out << "Network stats:\nPending send buffers: "
<< stats.npending_sends << "\nBytes in buffers: "
<< stats.nbytes_pending_sends << "\n";
if (utils::lowercase(parameters) == "all") {
*out << network::get_bandwidth_stats_all();
} else {
*out << network::get_bandwidth_stats(); // stats from previuos hour
}
}
void server::adminmsg_handler(const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters == "") {
*out << "You must type a message.";
return;
}
const std::string& sender = issuer_name;
const std::string& message = parameters;
LOG_SERVER << "Admin message: <" << sender << (message.find("/me ") == 0
? std::string(message.begin() + 3, message.end()) + ">"
: "> " + message) << "\n";
simple_wml::document data;
simple_wml::node& msg = data.root().add_child("whisper");
msg.set_attr_dup("sender", ("admin message from " + sender).c_str());
msg.set_attr_dup("message", message.c_str());
int n = 0;
for (wesnothd::player_map::const_iterator pl = players_.begin(); pl != players_.end(); ++pl) {
if (pl->second.is_moderator()) {
++n;
send_doc(data, pl->first);
}
}
if (n == 0) {
*out << "Sorry, no admin available right now. But your message got logged.";
return;
}
*out << "Message sent to " << n << " admins.";
}
void server::pm_handler(const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
std::string::iterator first_space = std::find(parameters.begin(), parameters.end(), ' ');
if (first_space == parameters.end()) {
*out << "You must name a receiver.";
return;
}
const std::string& sender = issuer_name;
const std::string receiver(parameters.begin(), first_space);
std::string message(first_space + 1, parameters.end());
utils::strip(message);
if (message.empty()) {
*out << "You must type a message.";
return;
}
simple_wml::document data;
simple_wml::node& msg = data.root().add_child("whisper");
// This string is parsed by the client!
msg.set_attr_dup("sender", ("server message from " + sender).c_str());
msg.set_attr_dup("message", message.c_str());
for (wesnothd::player_map::const_iterator pl = players_.begin(); pl != players_.end(); ++pl) {
if (receiver != pl->second.name().c_str()) {
continue;
}
send_doc(data, pl->first);
*out << "Message to " << receiver << " successfully sent.";
return;
}
*out << "No such nick: " << receiver;
}
void server::msg_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters == "") {
*out << "You must type a message.";
return;
}
rooms_.lobby().send_server_message_to_all(parameters);
for (std::vector<wesnothd::game*>::const_iterator g = games_.begin(); g != games_.end(); ++g) {
(*g)->send_server_message_to_all(parameters);
}
LOG_SERVER << "<server" << (parameters.find("/me ") == 0
? std::string(parameters.begin() + 3, parameters.end()) + ">"
: "> " + parameters) << "\n";
*out << "message '" << parameters << "' relayed to players";
}
void server::lobbymsg_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters == "") {
*out << "You must type a message.";
return;
}
rooms_.lobby().send_server_message_to_all(parameters);
LOG_SERVER << "<server" << (parameters.find("/me ") == 0
? std::string(parameters.begin() + 3, parameters.end()) + ">"
: "> " + parameters) << "\n";
*out << "message '" << parameters << "' relayed to players";
}
void server::status_handler(const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
*out << "STATUS REPORT for '" << parameters << "'";
bool found_something = false;
// If a simple username is given we'll check for its IP instead.
if (utils::isvalid_username(parameters)) {
for (wesnothd::player_map::const_iterator pl = players_.begin(); pl != players_.end(); ++pl) {
if (parameters == pl->second.name()) {
parameters = network::ip_address(pl->first);
found_something = true;
break;
}
}
if (!found_something) {
//out << "\nNo match found. You may want to check with 'searchlog'.";
//return out.str();
*out << process_command("searchlog " + parameters, issuer_name);
return;
}
}
const bool match_ip = (std::count(parameters.begin(), parameters.end(), '.') >= 1);
for (wesnothd::player_map::const_iterator pl = players_.begin(); pl != players_.end(); ++pl) {
if (parameters == "" || parameters == "*"
|| (match_ip && utils::wildcard_string_match(network::ip_address(pl->first), parameters))
|| (!match_ip && utils::wildcard_string_match(pl->second.name(), parameters))) {
found_something = true;
*out << std::endl << player_status(pl);
}
}
if (!found_something) *out << "\nNo match found. You may want to check with 'searchlog'.";
}
void server::clones_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& /*parameters*/, std::ostringstream *out) {
assert(out != NULL);
*out << "CLONES STATUS REPORT";
std::set<std::string> clones;
for (wesnothd::player_map::const_iterator pl = players_.begin(); pl != players_.end(); ++pl) {
if (clones.find(network::ip_address(pl->first)) != clones.end()) continue;
bool found = false;
for (wesnothd::player_map::const_iterator clone = boost::next(pl); clone != players_.end(); ++clone) {
if (network::ip_address(pl->first) == network::ip_address(clone->first)) {
if (!found) {
found = true;
clones.insert(network::ip_address(pl->first));
*out << std::endl << player_status(pl);
}
*out << std::endl << player_status(clone);
}
}
}
if (clones.empty()) {
*out << "No clones found.";
}
}
void server::bans_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters.empty()) {
ban_manager_.list_bans(*out);
} else if (utils::lowercase(parameters) == "deleted") {
ban_manager_.list_deleted_bans(*out);
} else if (utils::lowercase(parameters).find("deleted") == 0) {
std::string mask = parameters.substr(7);
ban_manager_.list_deleted_bans(*out, utils::strip(mask));
} else {
ban_manager_.list_bans(*out, utils::strip(parameters));
}
}
void server::ban_handler(const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
bool banned = false;
std::string::iterator first_space = std::find(parameters.begin(), parameters.end(), ' ');
if (first_space == parameters.end()) {
*out << ban_manager_.get_ban_help();
return;
}
std::string::iterator second_space = std::find(first_space + 1, parameters.end(), ' ');
const std::string target(parameters.begin(), first_space);
const std::string duration(first_space + 1, second_space);
time_t parsed_time = time(NULL);
if (ban_manager_.parse_time(duration, &parsed_time) == false) {
*out << "Failed to parse the ban duration: '" << duration << "'\n"
<< ban_manager_.get_ban_help();
return;
}
if (second_space == parameters.end()) {
--second_space;
}
std::string reason(second_space + 1, parameters.end());
utils::strip(reason);
if (reason.empty()) {
*out << "You need to give a reason for the ban.";
return;
}
std::string dummy_group;
// if we find a '.' consider it an ip mask
/** @todo FIXME: make a proper check for valid IPs. */
if (std::count(target.begin(), target.end(), '.') >= 1) {
banned = true;
*out << ban_manager_.ban(target, parsed_time, reason, issuer_name, dummy_group);
} else {
for (wesnothd::player_map::const_iterator pl = players_.begin();
pl != players_.end(); ++pl)
{
if (utils::wildcard_string_match(pl->second.name(), target)) {
if (banned) *out << "\n";
else banned = true;
const std::string ip = network::ip_address(pl->first);
*out << ban_manager_.ban(ip, parsed_time, reason, issuer_name, dummy_group, target);
}
}
if (!banned) {
// If nobody was banned yet check the ip_log but only if a
// simple username was used to prevent accidental bans.
// @todo FIXME: since we can have several entries now we should only ban the latest or so
/*if (utils::isvalid_username(target)) {
for (std::deque<connection_log>::const_iterator i = ip_log_.begin();
i != ip_log_.end(); ++i) {
if (i->nick == target) {
if (banned) out << "\n";
else banned = true;
out << ban_manager_.ban(i->ip, parsed_time, reason, issuer_name, group, target);
}
}
}*/
if(!banned) {
*out << "Nickmask '" << target << "' did not match, no bans set.";
}
}
}
}
void server::kickban_handler(const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
bool banned = false;
std::string::iterator first_space = std::find(parameters.begin(), parameters.end(), ' ');
if (first_space == parameters.end()) {
*out << ban_manager_.get_ban_help();
return;
}
std::string::iterator second_space = std::find(first_space + 1, parameters.end(), ' ');
const std::string target(parameters.begin(), first_space);
const std::string duration(first_space + 1, second_space);
time_t parsed_time = time(NULL);
if (ban_manager_.parse_time(duration, &parsed_time) == false) {
*out << "Failed to parse the ban duration: '" << duration << "'\n"
<< ban_manager_.get_ban_help();
return;
}
if (second_space == parameters.end()) {
--second_space;
}
std::string reason(second_space + 1, parameters.end());
utils::strip(reason);
if (reason.empty()) {
*out << "You need to give a reason for the ban.";
return;
}
std::string dummy_group;
// if we find a '.' consider it an ip mask
/** @todo FIXME: make a proper check for valid IPs. */
if (std::count(target.begin(), target.end(), '.') >= 1) {
banned = true;
*out << ban_manager_.ban(target, parsed_time, reason, issuer_name, dummy_group);
for (wesnothd::player_map::const_iterator pl = players_.begin();
pl != players_.end(); ++pl)
{
if (utils::wildcard_string_match(network::ip_address(pl->first), target)) {
*out << "\nKicked " << pl->second.name() << " ("
<< network::ip_address(pl->first) << ").";
send_error(pl->first, "You have been banned. Reason: " + reason);
network::queue_disconnect(pl->first);
}
}
} else {
for (wesnothd::player_map::const_iterator pl = players_.begin();
pl != players_.end(); ++pl)
{
if (utils::wildcard_string_match(pl->second.name(), target)) {
if (banned) *out << "\n";
else banned = true;
const std::string ip = network::ip_address(pl->first);
*out << ban_manager_.ban(ip, parsed_time, reason, issuer_name, dummy_group, target);
*out << "\nKicked " << pl->second.name() << " (" << ip << ").";
send_error(pl->first, "You have been banned. Reason: " + reason);
network::queue_disconnect(pl->first);
}
}
if (!banned) {
// If nobody was banned yet check the ip_log but only if a
// simple username was used to prevent accidental bans.
// @todo FIXME: since we can have several entries now we should only ban the latest or so
/*if (utils::isvalid_username(target)) {
for (std::deque<connection_log>::const_iterator i = ip_log_.begin();
i != ip_log_.end(); ++i) {
if (i->nick == target) {
if (banned) out << "\n";
else banned = true;
out << ban_manager_.ban(i->ip, parsed_time, reason, issuer_name, group, target);
}
}
}*/
if(!banned) {
*out << "Nickmask '" << target << "' did not match, no bans set.";
}
}
}
}
void server::gban_handler(const std::string& issuer_name, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
bool banned = false;
std::string::iterator first_space = std::find(parameters.begin(), parameters.end(), ' ');
if (first_space == parameters.end()) {
*out << ban_manager_.get_ban_help();
return;
}
std::string::iterator second_space = std::find(first_space + 1, parameters.end(), ' ');
const std::string target(parameters.begin(), first_space);
std::string group = std::string(first_space + 1, second_space);
first_space = second_space;
second_space = std::find(first_space + 1, parameters.end(), ' ');
const std::string duration(first_space + 1, second_space);
time_t parsed_time = time(NULL);
if (ban_manager_.parse_time(duration, &parsed_time) == false) {
*out << "Failed to parse the ban duration: '" << duration << "'\n"
<< ban_manager_.get_ban_help();
return;
}
if (second_space == parameters.end()) {
--second_space;
}
std::string reason(second_space + 1, parameters.end());
utils::strip(reason);
if (reason.empty()) {
*out << "You need to give a reason for the ban.";
return;
}
// if we find a '.' consider it an ip mask
/** @todo FIXME: make a proper check for valid IPs. */
if (std::count(target.begin(), target.end(), '.') >= 1) {
banned = true;
*out << ban_manager_.ban(target, parsed_time, reason, issuer_name, group);
} else {
for (wesnothd::player_map::const_iterator pl = players_.begin();
pl != players_.end(); ++pl)
{
if (utils::wildcard_string_match(pl->second.name(), target)) {
if (banned) *out << "\n";
else banned = true;
const std::string ip = network::ip_address(pl->first);
*out << ban_manager_.ban(ip, parsed_time, reason, issuer_name, group, target);
}
}
if (!banned) {
// If nobody was banned yet check the ip_log but only if a
// simple username was used to prevent accidental bans.
// @todo FIXME: since we can have several entries now we should only ban the latest or so
/*if (utils::isvalid_username(target)) {
for (std::deque<connection_log>::const_iterator i = ip_log_.begin();
i != ip_log_.end(); ++i) {
if (i->nick == target) {
if (banned) out << "\n";
else banned = true;
out << ban_manager_.ban(i->ip, parsed_time, reason, issuer_name, group, target);
}
}
}*/
if(!banned) {
*out << "Nickmask '" << target << "' did not match, no bans set.";
}
}
}
}
void server::unban_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters == "") {
*out << "You must enter an ipmask to unban.";
return;
}
ban_manager_.unban(*out, parameters);
}
void server::ungban_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters == "") {
*out << "You must enter an ipmask to ungban.";
return;
}
ban_manager_.unban_group(*out, parameters);
}
void server::kick_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters == "") {
*out << "You must enter a mask to kick.";
return;
}
std::string::iterator i = std::find(parameters.begin(), parameters.end(), ' ');
const std::string kick_mask = std::string(parameters.begin(), i);
const std::string kick_message =
(i == parameters.end() ? "You have been kicked."
: "You have been kicked. Reason: " + std::string(i + 1, parameters.end()));
bool kicked = false;
// if we find a '.' consider it an ip mask
const bool match_ip = (std::count(kick_mask.begin(), kick_mask.end(), '.') >= 1);
for (wesnothd::player_map::const_iterator pl = players_.begin();
pl != players_.end(); ++pl)
{
if ((match_ip && utils::wildcard_string_match(network::ip_address(pl->first), kick_mask))
|| (!match_ip && utils::wildcard_string_match(pl->second.name(), kick_mask))) {
if (kicked) *out << "\n";
else kicked = true;
*out << "Kicked " << pl->second.name() << " ("
<< network::ip_address(pl->first) << "). '"
<< kick_message << "'";
send_error(pl->first, kick_message);
network::queue_disconnect(pl->first);
}
}
if (!kicked) *out << "No user matched '" << kick_mask << "'.";
}
void server::motd_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters == "") {
if (motd_ != "") {
*out << "Message of the day:\n" << motd_;
return;
} else {
*out << "No message of the day set.";
return;
}
}
motd_ = parameters;
*out << "Message of the day set to: " << motd_;
}
void server::searchlog_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters.empty()) {
*out << "You must enter a mask to search for.";
return;
}
*out << "IP/NICK LOG for '" << parameters << "'";
bool found_something = false;
// If this looks like an IP look up which nicks have been connected from it
// Otherwise look for the last IP the nick used to connect
const bool match_ip = (std::count(parameters.begin(), parameters.end(), '.') >= 1);
for (std::deque<connection_log>::const_iterator i = ip_log_.begin();
i != ip_log_.end(); ++i) {
const std::string& username = i->nick;
const std::string& ip = i->ip;
if ((match_ip && utils::wildcard_string_match(ip, parameters))
|| (!match_ip && utils::wildcard_string_match(username, parameters))) {
found_something = true;
wesnothd::player_map::const_iterator pl = std::find_if(players_.begin(), players_.end(), boost::bind(&::match_user, _1, username, ip));
if (pl != players_.end()) {
*out << std::endl << player_status(pl);
} else {
*out << "\n'" << username << "' @ " << ip << " last seen: " << lg::get_timestamp(i->log_off, "%H:%M:%S %d.%m.%Y");
}
}
}
if (!found_something) *out << "\nNo match found.";
}
void server::dul_handler(const std::string& /*issuer_name*/, const std::string& /*query*/, std::string& parameters, std::ostringstream *out) {
assert(out != NULL);
if (parameters == "") {
*out << "Unregistered login is " << (deny_unregistered_login_ ? "disallowed" : "allowed") << ".";
} else {
deny_unregistered_login_ = (utils::lowercase(parameters) == "yes");
*out << "Unregistered login is now " << (deny_unregistered_login_ ? "disallowed" : "allowed") << ".";
}
}
void server::process_nickserv(const network::connection sock, simple_wml::node& data) {
const wesnothd::player_map::iterator pl = players_.find(sock);
if (pl == players_.end()) {
DBG_SERVER << "ERROR: Could not find player with socket: " << sock << "\n";
return;
}
// Check if this server allows nick registration at all
if(!user_handler_) {
rooms_.lobby().send_server_message("This server does not allow username registration.", sock);
return;
}
if(data.child("register")) {
try {
(user_handler_->add_user(pl->second.name(), (*data.child("register"))["mail"].to_string(),
(*data.child("register"))["password"].to_string()));
std::stringstream msg;
msg << "Your username has been registered." <<
// Warn that providing an email address might be a good idea
((*data.child("register"))["mail"].empty() ?
" It is recommended that you provide an email address for password recovery." : "");
rooms_.lobby().send_server_message(msg.str(), sock);
// Mark the player as registered and send the other clients
// an update to dislpay this change
pl->second.mark_registered();
simple_wml::document diff;
make_change_diff(games_and_users_list_.root(), NULL,
"user", pl->second.config_address(), diff);
rooms_.lobby().send_data(diff);
} catch (user_handler::error& e) {
rooms_.lobby().send_server_message("There was an error registering your username. The error message was: "
+ e.message, sock);
}
return;
}
// A user requested to update his password or mail
if(data.child("set")) {
if(!(user_handler_->user_exists(pl->second.name()))) {
rooms_.lobby().send_server_message("You are not registered. Please register first.", sock);
return;
}
const simple_wml::node& set = *(data.child("set"));
try {
user_handler_->set_user_detail(pl->second.name(), set["detail"].to_string(), set["value"].to_string());
rooms_.lobby().send_server_message("Your details have been updated.", sock);
} catch (user_handler::error& e) {
rooms_.lobby().send_server_message("There was an error updating your details. The error message was: "
+ e.message, sock);
}
return;
}
// A user requested information about another user
if(data.child("details")) {
rooms_.lobby().send_server_message("Valid details for this server are: " +
user_handler_->get_valid_details(), sock);
return;
}
// A user requested a list of which details can be set
if(data.child("info")) {
try {
std::string res = user_handler_->user_info((*data.child("info"))["name"].to_string());
rooms_.lobby().send_server_message(res, sock);
} catch (user_handler::error& e) {
rooms_.lobby().send_server_message("There was an error looking up the details of the user '" +
(*data.child("info"))["name"].to_string() + "'. " +" The error message was: "
+ e.message, sock);
}
return;
}
// A user requested to delete his nick
if(data.child("drop")) {
if(!(user_handler_->user_exists(pl->second.name()))) {
rooms_.lobby().send_server_message("You are not registered.", sock);
return;
}
// With the current policy of dissallowing to log in with a
// registerd username without the password we should never get
// to call this
if(!(pl->second.registered())) {
rooms_.lobby().send_server_message("You are not logged in.", sock);
return;
}
try {
user_handler_->remove_user(pl->second.name());
rooms_.lobby().send_server_message("Your username has been dropped.", sock);
// Mark the player as not registered and send the other clients
// an update to dislpay this change
pl->second.mark_registered(false);
simple_wml::document diff;
make_change_diff(games_and_users_list_.root(), NULL,
"user", pl->second.config_address(), diff);
rooms_.lobby().send_data(diff);
} catch (user_handler::error& e) {
rooms_.lobby().send_server_message("There was an error dropping your username. The error message was: "
+ e.message, sock);
}
return;
}
}
void server::process_whisper(const network::connection sock,
simple_wml::node& whisper) const {
if ((whisper["receiver"] == "") || (whisper["message"] == "")) {
static simple_wml::document data(
"[message]\n"
"message=\"Invalid number of arguments\"\n"
"sender=\"server\"\n"
"[/message]\n", simple_wml::INIT_COMPRESSED);
send_doc(data, sock);
return;
}
const wesnothd::player_map::const_iterator pl = players_.find(sock);
if (pl == players_.end()) {
ERR_SERVER << "ERROR: Could not find whispering player. (socket: "
<< sock << ")\n";
return;
}
whisper.set_attr_dup("sender", pl->second.name().c_str());
bool dont_send = false;
const simple_wml::string_span& whisper_receiver = whisper["receiver"];
for (wesnothd::player_map::const_iterator i = players_.begin(); i != players_.end(); ++i) {
if (whisper_receiver != i->second.name().c_str()) {
continue;
}
std::vector<wesnothd::game*>::const_iterator g;
for (g = games_.begin(); g != games_.end(); ++g) {
if (!(*g)->is_member(i->first)) continue;
// Don't send to players in a running game the sender is part of.
dont_send = ((*g)->started() && (*g)->is_player(i->first) && (*g)->is_member(sock));
break;
}
if (dont_send) {
break;
}
simple_wml::document cwhisper;
whisper.copy_into(cwhisper.root().add_child("whisper"));
send_doc(cwhisper, i->first);
return;
}
simple_wml::document data;
simple_wml::node& msg = data.root().add_child("message");
if (dont_send) {
msg.set_attr("message", "You cannot send private messages to players in a running game you observe.");
} else {
msg.set_attr_dup("message", ("Can't find '" + whisper["receiver"].to_string() + "'.").c_str());
}
msg.set_attr("sender", "server");
send_doc(data, sock);
}
void server::process_data_lobby(const network::connection sock,
simple_wml::document& data) {
DBG_SERVER << "in process_data_lobby...\n";
const wesnothd::player_map::iterator pl = players_.find(sock);
if (pl == players_.end()) {
ERR_SERVER << "ERROR: Could not find player in players_. (socket: "
<< sock << ")\n";
return;
}
if (const simple_wml::node* create_game = data.child("create_game")) {
if (graceful_restart) {
static simple_wml::document leave_game_doc("[leave_game]\n[/leave_game]\n", simple_wml::INIT_COMPRESSED);
send_doc(leave_game_doc, sock);
rooms_.lobby().send_server_message("This server is shutting down. You aren't allowed to make new games. Please reconnect to the new server.", sock);
send_doc(games_and_users_list_, sock);
return;
}
const std::string game_name = (*create_game)["name"].to_string();
const std::string game_password = (*create_game)["password"].to_string();
DBG_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tcreates a new game: \"" << game_name << "\".\n";
// Create the new game, remove the player from the lobby
// and set the player as the host/owner.
games_.push_back(new wesnothd::game(players_, sock, game_name, save_replays_, replay_save_path_));
wesnothd::game& g = *games_.back();
if(game_password.empty() == false) {
g.set_password(game_password);
}
create_game->copy_into(g.level().root());
rooms_.exit_lobby(sock);
simple_wml::document diff;
if(make_change_diff(games_and_users_list_.root(), NULL,
"user", pl->second.config_address(), diff)) {
rooms_.lobby().send_data(diff);
}
return;
}
// See if the player is joining a game
if (const simple_wml::node* join = data.child("join")) {
const bool observer = join->attr("observe").to_bool();
const std::string& password = (*join)["password"].to_string();
int game_id = (*join)["id"].to_int();
const std::vector<wesnothd::game*>::iterator g =
std::find_if(games_.begin(), games_.end(), wesnothd::game_id_matches(game_id));
static simple_wml::document leave_game_doc("[leave_game]\n[/leave_game]\n", simple_wml::INIT_COMPRESSED);
if (g == games_.end()) {
WRN_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tattempted to join unknown game:\t" << game_id << ".\n";
send_doc(leave_game_doc, sock);
rooms_.lobby().send_server_message("Attempt to join unknown game.", sock);
send_doc(games_and_users_list_, sock);
return;
} else if (!(*g)->level_init()) {
WRN_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tattempted to join uninitialized game:\t\"" << (*g)->name()
<< "\" (" << game_id << ").\n";
send_doc(leave_game_doc, sock);
rooms_.lobby().send_server_message("Attempt to join an uninitialized game.", sock);
send_doc(games_and_users_list_, sock);
return;
} else if (pl->second.is_moderator()) {
// Admins are always allowed to join.
} else if ((*g)->player_is_banned(sock)) {
DBG_SERVER << network::ip_address(sock) << "\tReject banned player: "
<< pl->second.name() << "\tfrom game:\t\"" << (*g)->name()
<< "\" (" << game_id << ").\n";
send_doc(leave_game_doc, sock);
rooms_.lobby().send_server_message("You are banned from this game.", sock);
send_doc(games_and_users_list_, sock);
return;
} else if(!observer && !(*g)->password_matches(password)) {
WRN_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tattempted to join game:\t\"" << (*g)->name() << "\" ("
<< game_id << ") with bad password\n";
send_doc(leave_game_doc, sock);
rooms_.lobby().send_server_message("Incorrect password.", sock);
send_doc(games_and_users_list_, sock);
return;
}
// Hack to work around problems of players not getting properly removed
// from a game. Still need to figure out actual cause...
const std::vector<wesnothd::game*>::iterator g2 =
std::find_if(games_.begin(), games_.end(), wesnothd::game_is_member(sock));
if (g2 != games_.end()) {
WRN_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tattempted to join a second game. He's already in game:\t\""
<< (*g2)->name() << "\" (" << (*g2)->id()
<< ") and the lobby. (socket: " << sock << ")\n"
<< "Removing him from that game to fix the inconsistency...\n";
(*g2)->remove_player(sock);
}
bool joined = (*g)->add_player(sock, observer);
if (!joined) {
WRN_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tattempted to observe game:\t\"" << (*g)->name() << "\" ("
<< game_id << ") which doesn't allow observers.\n";
send_doc(leave_game_doc, sock);
rooms_.lobby().send_server_message("Attempt to observe a game that doesn't allow observers. (You probably joined the game shortly after it filled up.)", sock);
send_doc(games_and_users_list_, sock);
return;
}
rooms_.exit_lobby(sock);
(*g)->describe_slots();
//send notification of changes to the game and user
simple_wml::document diff;
bool diff1 = make_change_diff(*games_and_users_list_.child("gamelist"),
"gamelist", "game", (*g)->description(), diff);
bool diff2 = make_change_diff(games_and_users_list_.root(), NULL,
"user", pl->second.config_address(), diff);
if (diff1 || diff2) {
rooms_.lobby().send_data(diff);
}
}
if (data.child("room_join")) {
rooms_.process_room_join(data, pl);
}
if (data.child("room_part")) {
rooms_.process_room_part(data, pl);
}
if (data.child("room_query")) {
rooms_.process_room_query(data, pl);
}
// See if it's a message, in which case we add the name of the sender,
// and forward it to all players in the lobby.
if (data.child("message")) {
rooms_.process_message(data, pl);
}
// Player requests update of lobby content,
// for example when cancelling the create game dialog
if (data.child("refresh_lobby")) {
send_doc(games_and_users_list_, sock);
}
}
/**
* Process data sent from a member of a game.
*/
void server::process_data_game(const network::connection sock,
simple_wml::document& data) {
DBG_SERVER << "in process_data_game...\n";
const wesnothd::player_map::iterator pl = players_.find(sock);
if (pl == players_.end()) {
ERR_SERVER << "ERROR: Could not find player in players_. (socket: "
<< sock << ")\n";
return;
}
const std::vector<wesnothd::game*>::iterator itor =
std::find_if(games_.begin(), games_.end(), wesnothd::game_is_member(sock));
if (itor == games_.end()) {
ERR_SERVER << "ERROR: Could not find game for player: "
<< pl->second.name() << ". (socket: " << sock << ")\n";
return;
}
wesnothd::game* g = *itor;
// If this is data describing the level for a game.
if (data.child("side")) {
if (!g->is_owner(sock)) {
return;
}
size_t nsides = 0;
const simple_wml::node::child_list& sides = data.root().children("side");
for (simple_wml::node::child_list::const_iterator s = sides.begin(); s != sides.end(); ++s) {
++nsides;
}
if (nsides > gamemap::MAX_PLAYERS) {
delete_game(itor);
std::stringstream msg;
msg << "This server does not support games with more than "
<< gamemap::MAX_PLAYERS << " sides. Game aborted.";
rooms_.lobby().send_server_message(msg.str(), sock);
return;
}
// If this game is having its level data initialized
// for the first time, and is ready for players to join.
// We should currently have a summary of the game in g->level().
// We want to move this summary to the games_and_users_list_, and
// place a pointer to that summary in the game's description.
// g->level() should then receive the full data for the game.
if (!g->level_init()) {
LOG_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tcreated game:\t\"" << g->name() << "\" ("
<< g->id() << ").\n";
// Update our config object which describes the open games,
// and save a pointer to the description in the new game.
simple_wml::node* const gamelist = games_and_users_list_.child("gamelist");
assert(gamelist != NULL);
simple_wml::node& desc = gamelist->add_child("game");
g->level().root().copy_into(desc);
if (const simple_wml::node* m = data.child("multiplayer")) {
m->copy_into(desc);
} else {
WRN_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tsent scenario data in game:\t\"" << g->name() << "\" ("
<< g->id() << ") without a 'multiplayer' child.\n";
// Set the description so it can be removed in delete_game().
g->set_description(&desc);
delete_game(itor);
rooms_.lobby().send_server_message("The scenario data is missing the [multiplayer] tag which contains the game settings. Game aborted.", sock);
return;
}
g->set_description(&desc);
desc.set_attr_dup("id", lexical_cast<std::string>(g->id()).c_str());
} else {
WRN_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tsent scenario data in game:\t\"" << g->name() << "\" ("
<< g->id() << ") although it's already initialized.\n";
return;
}
assert(games_and_users_list_.child("gamelist")->children("game").empty() == false);
simple_wml::node& desc = *g->description();
// Update the game's description.
// If there is no shroud, then tell players in the lobby
// what the map looks like
if (!data["mp_shroud"].to_bool()) {
desc.set_attr_dup("map_data", data["map_data"]);
}
if (const simple_wml::node* e = data.child("era")) {
if (!e->attr("require_era").to_bool(true)) {
desc.set_attr("require_era", "no");
}
}
// Record the full scenario in g->level()
g->level().swap(data);
// The host already put himself in the scenario so we just need
// to update_side_data().
//g->take_side(sock);
g->update_side_data();
g->describe_slots();
assert(games_and_users_list_.child("gamelist")->children("game").empty() == false);
// Send the update of the game description to the lobby.
simple_wml::document diff;
make_add_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", diff);
rooms_.lobby().send_data(diff);
/** @todo FIXME: Why not save the level data in the history_? */
return;
// Everything below should only be processed if the game is already intialized.
} else if (!g->level_init()) {
WRN_SERVER << network::ip_address(sock) << "\tReceived unknown data from: "
<< pl->second.name() << " (socket:" << sock
<< ") while the scenario wasn't yet initialized.\n" << data.output();
return;
// If the host is sending the next scenario data.
} else if (const simple_wml::node* scenario = data.child("store_next_scenario")) {
if (!g->is_owner(sock)) return;
if (!g->level_init()) {
WRN_SERVER << network::ip_address(sock) << "\tWarning: "
<< pl->second.name() << "\tsent [store_next_scenario] in game:\t\""
<< g->name() << "\" (" << g->id()
<< ") while the scenario is not yet initialized.";
return;
}
g->save_replay();
size_t nsides = 0;
const simple_wml::node::child_list& sides = scenario->children("side");
for (simple_wml::node::child_list::const_iterator s = sides.begin(); s != sides.end(); ++s) {
++nsides;
}
if (nsides > gamemap::MAX_PLAYERS) {
delete_game(itor);
std::stringstream msg;
msg << "This server does not support games with more than "
<< gamemap::MAX_PLAYERS << " sides.";
rooms_.lobby().send_server_message(msg.str(), sock);
return;
}
// Record the full scenario in g->level()
g->level().clear();
scenario->copy_into(g->level().root());
if (g->description() == NULL) {
ERR_SERVER << network::ip_address(sock) << "\tERROR: \""
<< g->name() << "\" (" << g->id()
<< ") is initialized but has no description_.\n";
return;
}
simple_wml::node& desc = *g->description();
// Update the game's description.
if (const simple_wml::node* m = scenario->child("multiplayer")) {
m->copy_into(desc);
} else {
WRN_SERVER << network::ip_address(sock) << "\t" << pl->second.name()
<< "\tsent scenario data in game:\t\"" << g->name() << "\" ("
<< g->id() << ") without a 'multiplayer' child.\n";
delete_game(itor);
rooms_.lobby().send_server_message("The scenario data is missing the [multiplayer] tag which contains the game settings. Game aborted.", sock);
return;
}
// If there is no shroud, then tell players in the lobby
// what the map looks like.
const simple_wml::node& s = g->level().root();
if (s["mp_shroud"].to_bool()) {
desc.set_attr_dup("map_data", s["map_data"]);
} else {
desc.set_attr("map_data", "");
}
if (const simple_wml::node* e = data.child("era")) {
if (!e->attr("require_era").to_bool(true)) {
desc.set_attr("require_era", "no");
}
}
// Send the update of the game description to the lobby.
update_game_in_lobby(g);
g->start_game(pl);
return;
// If a player advances to the next scenario of a mp campaign. (deprecated)
///@deprecated r22619 a player advances to the next scenario of a mp campaign (notify_next_scenario)
} else if(data.child("notify_next_scenario")) {
//g->send_data(g->construct_server_message(pl->second.name()
// + " advanced to the next scenario."), sock);
return;
// A mp client sends a request for the next scenario of a mp campaign.
} else if (data.child("load_next_scenario")) {
g->load_next_scenario(pl);
return;
} else if (data.child("start_game")) {
if (!g->is_owner(sock)) return;
// Send notification of the game starting immediately.
// g->start_game() will send data that assumes
// the [start_game] message has been sent
g->send_data(data, sock);
g->start_game(pl);
//update the game having changed in the lobby
update_game_in_lobby(g);
return;
} else if (data.child("leave_game")) {
// May be better to just let remove_player() figure out when a game ends.
if ((g->is_player(sock) && g->nplayers() == 1)
|| (g->is_owner(sock) && (!g->started() || g->nplayers() == 0))) {
// Remove the player in delete_game() with all other remaining
// ones so he gets the updated gamelist.
delete_game(itor);
} else {
g->remove_player(sock);
rooms_.enter_lobby(sock);
g->describe_slots();
// Send all other players in the lobby the update to the gamelist.
simple_wml::document diff;
bool diff1 = make_change_diff(*games_and_users_list_.child("gamelist"),
"gamelist", "game", g->description(), diff);
bool diff2 = make_change_diff(games_and_users_list_.root(), NULL,
"user", pl->second.config_address(), diff);
if (diff1 || diff2) {
rooms_.lobby().send_data(diff, sock);
}
// Send the player who has quit the gamelist.
send_doc(games_and_users_list_, sock);
}
return;
// If this is data describing side changes by the host.
} else if (const simple_wml::node* diff = data.child("scenario_diff")) {
if (!g->is_owner(sock)) return;
g->level().root().apply_diff(*diff);
const simple_wml::node* cfg_change = diff->child("change_child");
if (cfg_change && cfg_change->child("side")) {
g->update_side_data();
}
if (g->describe_slots()) {
update_game_in_lobby(g);
}
g->send_data(data, sock);
return;
// If a player changes his faction.
} else if (data.child("change_faction")) {
g->send_data(data, sock);
return;
// If the owner of a side is changing the controller.
} else if (const simple_wml::node *change = data.child("change_controller")) {
g->transfer_side_control(sock, *change);
if (g->describe_slots()) {
update_game_in_lobby(g);
}
// FIXME: Why not save it in the history_? (if successful)
return;
// If all observers should be muted. (toggles)
} else if (data.child("muteall")) {
if (!g->is_owner(sock)) {
g->send_server_message("You cannot mute: not the game host.", sock);
return;
}
g->mute_all_observers();
return;
// If an observer should be muted.
} else if (const simple_wml::node* mute = data.child("mute")) {
g->mute_observer(*mute, pl);
return;
// If an observer should be unmuted.
} else if (const simple_wml::node* unmute = data.child("unmute")) {
g->unmute_observer(*unmute, pl);
return;
// The owner is kicking/banning someone from the game.
} else if (data.child("kick") || data.child("ban")) {
bool ban = (data.child("ban") != NULL);
const network::connection user =
(ban ? g->ban_user(*data.child("ban"), pl)
: g->kick_member(*data.child("kick"), pl));
if (user) {
rooms_.enter_lobby(user);
if (g->describe_slots()) {
update_game_in_lobby(g, user);
}
// Send all other players in the lobby the update to the gamelist.
simple_wml::document diff;
make_change_diff(*games_and_users_list_.child("gamelist"),
"gamelist", "game", g->description(), diff);
const wesnothd::player_map::iterator pl2 = players_.find(user);
if (pl2 == players_.end()) {
ERR_SERVER << "ERROR: Could not find kicked player in players_."
" (socket: " << user << ")\n";
} else {
make_change_diff(games_and_users_list_.root(), NULL, "user",
pl2->second.config_address(), diff);
}
rooms_.lobby().send_data(diff, sock);
// Send the removed user the lobby game list.
send_doc(games_and_users_list_, user);
}
return;
} else if (const simple_wml::node* unban = data.child("unban")) {
g->unban_user(*unban, pl);
return;
// If info is being provided about the game state.
} else if (const simple_wml::node* info = data.child("info")) {
if (!g->is_player(sock)) return;
if ((*info)["type"] == "termination") {
g->set_termination_reason((*info)["condition"].to_string());
if ((*info)["condition"].to_string() == "out of sync") {
g->send_server_message_to_all(pl->second.name() + " reports out of sync errors.");
}
}
return;
} else if (data.child("turn")) {
// Notify the game of the commands, and if it changes
// the description, then sync the new description
// to players in the lobby.
if (g->process_turn(data, pl)) {
update_game_in_lobby(g);
}
return;
} else if (data.child("whiteboard")) {
g->process_whiteboard(data,pl);
return;
} else if (data.child("message")) {
g->process_message(data, pl);
return;
} else if (data.child("stop_updates")) {
g->send_data(data, sock);
return;
} else if (data.child("wait_global")) {
g->allow_global(data);
return;
// Data to ignore.
} else if (data.child("error")
|| data.child("side_secured")
|| data.root().has_attr("failed")
|| data.root().has_attr("side_drop")
|| data.root().has_attr("side")) {
return;
}
WRN_SERVER << network::ip_address(sock) << "\tReceived unknown data from: "
<< pl->second.name() << " (socket:" << sock << ") in game: \""
<< g->name() << "\" (" << g->id() << ")\n" << data.output();
}
void server::delete_game(std::vector<wesnothd::game*>::iterator game_it) {
metrics_.game_terminated((*game_it)->termination_reason());
simple_wml::node* const gamelist = games_and_users_list_.child("gamelist");
assert(gamelist != NULL);
// Send a diff of the gamelist with the game deleted to players in the lobby
simple_wml::document diff;
if(make_delete_diff(*gamelist, "gamelist", "game",
(*game_it)->description(), diff)) {
rooms_.lobby().send_data(diff);
}
// Delete the game from the games_and_users_list_.
const simple_wml::node::child_list& games = gamelist->children("game");
const simple_wml::node::child_list::const_iterator g =
std::find(games.begin(), games.end(), (*game_it)->description());
if (g != games.end()) {
const size_t index = g - games.begin();
gamelist->remove_child("game", index);
} else {
// Can happen when the game ends before the scenario was transferred.
LOG_SERVER << "Could not find game (" << (*game_it)->id()
<< ") to delete in games_and_users_list_.\n";
}
const wesnothd::user_vector& users = (*game_it)->all_game_users();
// Set the availability status for all quitting users.
for (wesnothd::user_vector::const_iterator user = users.begin();
user != users.end(); ++user)
{
const wesnothd::player_map::iterator pl = players_.find(*user);
if (pl != players_.end()) {
pl->second.mark_available();
simple_wml::document udiff;
if (make_change_diff(games_and_users_list_.root(), NULL,
"user", pl->second.config_address(), udiff)) {
rooms_.lobby().send_data(udiff);
}
} else {
ERR_SERVER << "ERROR: delete_game(): Could not find user in players_. (socket: "
<< *user << ")\n";
}
}
//send users in the game a notification to leave the game since it has ended
static simple_wml::document leave_game_doc("[leave_game]\n[/leave_game]\n", simple_wml::INIT_COMPRESSED);
(*game_it)->send_data(leave_game_doc);
// Put the remaining users back in the lobby.
rooms_.enter_lobby(**game_it);
(*game_it)->send_data(games_and_users_list_);
delete *game_it;
games_.erase(game_it);
}
void server::update_game_in_lobby(const wesnothd::game* g, network::connection exclude)
{
simple_wml::document diff;
if (make_change_diff(*games_and_users_list_.child("gamelist"), "gamelist", "game", g->description(), diff)) {
rooms_.lobby().send_data(diff, exclude);
}
}
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifndef _MSC_VER
#include <unistd.h>
#endif
int main(int argc, char** argv) {
int port = 15000;
size_t min_threads = 5;
size_t max_threads = 0;
srand(static_cast<unsigned>(time(NULL)));
std::string config_file;
// setting path to currentworking directory
game_config::path = get_cwd();
// show 'info' by default
lg::set_log_domain_severity("server", 2);
lg::timestamps(true);
for (int arg = 1; arg != argc; ++arg) {
const std::string val(argv[arg]);
if (val.empty()) {
continue;
}
if ((val == "--config" || val == "-c") && arg+1 != argc) {
config_file = argv[++arg];
} else if (val == "--verbose" || val == "-v") {
lg::set_log_domain_severity("all", 3);
} else if (val.substr(0, 6) == "--log-") {
size_t p = val.find('=');
if (p == std::string::npos) {
std::cerr << "unknown option: " << val << '\n';
return 2;
}
std::string s = val.substr(6, p - 6);
int severity;
if (s == "error") severity = 0;
else if (s == "warning") severity = 1;
else if (s == "info") severity = 2;
else if (s == "debug") severity = 3;
else {
std::cerr << "unknown debug level: " << s << '\n';
return 2;
}
while (p != std::string::npos) {
size_t q = val.find(',', p + 1);
s = val.substr(p + 1, q == std::string::npos ? q : q - (p + 1));
if (!lg::set_log_domain_severity(s, severity)) {
std::cerr << "unknown debug domain: " << s << '\n';
return 2;
}
p = q;
}
} else if ((val == "--port" || val == "-p") && arg+1 != argc) {
port = atoi(argv[++arg]);
} else if (val == "--help" || val == "-h") {
std::cout << "usage: " << argv[0]
<< " [-dvV] [-c path] [-m n] [-p port] [-t n]\n"
<< " -c, --config <path> Tells wesnothd where to find the config file to use.\n"
<< " -d, --daemon Runs wesnothd as a daemon.\n"
<< " -h, --help Shows this usage message.\n"
<< " --log-<level>=<domain1>,<domain2>,...\n"
<< " sets the severity level of the debug domains.\n"
<< " 'all' can be used to match any debug domain.\n"
<< " Available levels: error, warning, info, debug.\n"
<< " -p, --port <port> Binds the server to the specified port.\n"
<< " -t, --threads <n> Uses n worker threads for network I/O (default: 5).\n"
<< " -v --verbose Turns on more verbose logging.\n"
<< " -V, --version Returns the server version.\n";
return 0;
} else if (val == "--version" || val == "-V") {
std::cout << "Battle for Wesnoth server " << game_config::version
<< "\n";
return 0;
} else if (val == "--daemon" || val == "-d") {
#ifdef _WIN32
ERR_SERVER << "Running as a daemon is not supported on this platform\n";
return -1;
#else
const pid_t pid = fork();
if (pid < 0) {
ERR_SERVER << "Could not fork and run as a daemon\n";
return -1;
} else if (pid > 0) {
std::cout << "Started wesnothd as a daemon with process id "
<< pid << "\n";
return 0;
}
setsid();
#endif
} else if ((val == "--threads" || val == "-t") && arg+1 != argc) {
min_threads = atoi(argv[++arg]);
if (min_threads > 30) {
min_threads = 30;
}
} else if ((val == "--max-threads" || val == "-T") && arg+1 != argc) {
max_threads = atoi(argv[++arg]);
} else if(val == "--request_sample_frequency" && arg+1 != argc) {
request_sample_frequency = atoi(argv[++arg]);
} else {
ERR_SERVER << "unknown option: " << val << "\n";
return 2;
}
}
network::set_raw_data_only();
try {
server(port, config_file, min_threads, max_threads).run();
} catch(network::error& e) {
ERR_SERVER << "Caught network error while server was running. Aborting.: "
<< e.message << "\n";
/**
* @todo errno should be passed here with the error or it might not be
* the true errno anymore. Seems to work good enough for now though.
*/
return errno;
}
return 0;
}