/* $Id$ */ /* Copyright (C) 2003 - 2008 by David White 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 version 2 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 server/server.cpp * Wesnoth-Server, for multiplayer-games. */ #include "../global.hpp" #include "../config.hpp" #include "../game_config.hpp" #include "../log.hpp" #include "../map.hpp" // gamemap::MAX_PLAYERS #include "../network.hpp" #include "../filesystem.hpp" #include "../time.hpp" #include "../serialization/parser.hpp" #include "../serialization/preprocessor.hpp" #include "../serialization/string_utils.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" // For now sample_user_handler is broken due // to the hardcoding of the phpbb hashing algorithms // #include "sample_user_handler.hpp" #ifdef HAVE_MYSQLPP #include "forum_user_handler.hpp" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _WIN32 #include 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 /** * fatal and directly server related errors/warnings, * ie not caused by erroneous client data */ #define ERR_SERVER LOG_STREAM(err, mp_server) /** clients send wrong/unexpected data */ #define WRN_SERVER LOG_STREAM(warn, mp_server) /** normal events */ #define LOG_SERVER LOG_STREAM(info, mp_server) #define DBG_SERVER LOG_STREAM(debug, mp_server) #define ERR_CONFIG LOG_STREAM(err, config) #define WRN_CONFIG LOG_STREAM(warn, 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; static void reload_config(int signal) { assert(signal == SIGHUP); config_reload = 1; } static void exit_sigint(int signal) { assert(signal == SIGINT); LOG_SERVER << "SIGINT caught, exiting without cleanup immediately.\n"; exit(1); } static void exit_sigterm(int signal) { assert(signal == SIGTERM); LOG_SERVER << "SIGTERM caught, exiting without cleanup immediately.\n"; exit(1); } 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(); simple_wml::string_span s = doc.output_compressed(); network::send_raw_data(s.begin(), s.size(), connection, type); } void make_add_diff(const simple_wml::node& src, const char* gamelist, const char* type, simple_wml::document& out, int index=-1) { if(out.root().child("gamelist_diff") == NULL) { out.root().add_child("gamelist_diff"); } simple_wml::node* top = out.root().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(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.root().child("gamelist_diff") == NULL) { out.root().add_child("gamelist_diff"); } simple_wml::node* top = out.root().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.root().child("gamelist_diff") == NULL) { out.root().add_child("gamelist_diff"); } simple_wml::node* top = out.root().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; } } 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; } }; class server { public: server(int port, const std::string& config_file, size_t min_threads,size_t max_threads); void run(); private: void send_error(network::connection sock, const char* msg) const; void send_error_dup(network::connection sock, const std::string& msg) const; // The same as send_error(), we just add an extra child to the response // telling the client the chosen username requires a password. void send_password_request(network::connection sock, const char* msg, const std::string& user); const network::manager net_manager_; network::server_manager server_; wesnothd::ban_manager ban_manager_; std::map ip_log_; boost::scoped_ptr user_handler_; std::map seeds_; /** std::map. */ wesnothd::player_map players_; wesnothd::player_map ghost_players_ ; std::vector games_; wesnothd::game not_logged_in_; /** The lobby is implemented as a game. */ wesnothd::game lobby_; /** server socket/fifo. */ boost::scoped_ptr input_; const std::string config_file_; config cfg_; /** Read the server config from file 'config_file_'. */ config read_config() const; // settings from the server config std::set accepted_versions_; std::map redirected_versions_; std::map proxy_versions_; std::vector disallowed_names_; std::string admin_passwd_; std::set admins_; std::string motd_; size_t default_max_messages_; size_t default_time_period_; size_t concurrent_connections_; bool graceful_restart; time_t lan_server_; time_t last_user_seen_time_; std::string restart_command; size_t max_ip_log_size_; /** Parse the server config into local variables. */ void load_config(); bool ip_exceeds_connection_limit(const std::string& ip) const; bool is_ip_banned(const std::string& ip) const; simple_wml::document version_query_response_; simple_wml::document login_response_; simple_wml::document join_lobby_response_; simple_wml::document games_and_users_list_; metrics metrics_; time_t last_ping_; time_t last_stats_; void dump_stats(const time_t& now); time_t last_uh_clean_; void clean_user_handler(const time_t& now); void process_data(const network::connection sock, simple_wml::document& data); void process_login(const network::connection sock, simple_wml::document& data); /** Handle queries from clients. */ void process_query(const network::connection sock, simple_wml::node& query); /** Process commands from admins and users. */ std::string process_command(const std::string& cmd, const std::string& issuer_name); /** Handle private messages between players. */ void process_whisper(const network::connection sock, simple_wml::node& whisper) const; /** Handle nickname registration related requests from clients. */ void process_nickserv(const network::connection sock, simple_wml::node& data); void process_data_lobby(const network::connection sock, simple_wml::document& data); void process_data_game(const network::connection sock, simple_wml::document& data); void delete_game(std::vector::iterator game_it); void update_game_in_lobby(const wesnothd::game* g, network::connection exclude=0); void start_new_server(); }; 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_(), user_handler_(NULL), seeds_(), players_(), ghost_players_(), games_(), not_logged_in_(players_), lobby_(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), 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_) { load_config(); ban_manager_.read(); #ifndef _MSC_VER signal(SIGHUP, reload_config); #endif signal(SIGINT, exit_sigint); signal(SIGTERM, exit_sigterm); } void server::send_error(network::connection sock, const char* msg) const { simple_wml::document doc; doc.root().add_child("error").set_attr("message", msg); simple_wml::string_span output = doc.output_compressed(); network::send_raw_data(output.begin(), output.size(), sock, "error"); } void server::send_error_dup(network::connection sock, const std::string& msg) const { simple_wml::document doc; doc.root().add_child("error").set_attr_dup("message", msg.c_str()); simple_wml::string_span output = doc.output_compressed(); network::send_raw_data(output.begin(), output.size(), sock, "error"); } void server::send_password_request(network::connection sock, const char* msg, const std::string& user) { std::string salt1 = user_handler_->create_salt(); std::string salt2 = user_handler_->create_pepper(user, 0); std::string salt3 = user_handler_->create_pepper(user, 1); seeds_.insert(std::pair(sock, salt1)); simple_wml::document doc; doc.root().add_child("error").set_attr("message", msg); (*(doc.root().child("error"))).set_attr("password_request", "yes"); (*(doc.root().child("error"))).set_attr("random_salt", salt1.c_str()); (*(doc.root().child("error"))).set_attr("hash_seed", salt2.c_str()); (*(doc.root().child("error"))).set_attr("salt", salt3.c_str()); simple_wml::string_span output = doc.output_compressed(); network::send_raw_data(output.begin(), output.size(), sock); } config server::read_config() const { config configuration; if (config_file_ == "") return configuration; scoped_istream stream = preprocess_file(config_file_); std::string errors; try { read(configuration, *stream, &errors); if (errors.empty()) { LOG_SERVER << "Server configuration from file: '" << config_file_ << "' read.\n"; } else { ERR_CONFIG << "ERROR: Errors reading configuration file: '" << errors << "'.\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 # warning "No FIFODIR set" # define FIFODIR "/var/run/wesnothd" #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)); admin_passwd_ = cfg_["passwd"]; motd_ = cfg_["motd"]; lan_server_ = lexical_cast_default(cfg_["lan_server"], 0); 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_ = lexical_cast_default(cfg_["max_messages"],4); default_time_period_ = lexical_cast_default(cfg_["messages_time_period"],10); concurrent_connections_ = lexical_cast_default(cfg_["connections_allowed"],5); max_ip_log_size_ = lexical_cast_default(cfg_["max_ip_log_size"],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"]; ntime::source::get_source().set_frame_time(lexical_cast_default(cfg_["ms_per_frame"],20)); accepted_versions_.clear(); const std::string& versions = cfg_["versions_accepted"]; if (versions.empty() == false) { const std::vector accepted(utils::split(versions)); for (std::vector::const_iterator i = accepted.begin(); i != accepted.end(); ++i) { accepted_versions_.insert(*i); } } else { accepted_versions_.insert(game_config::version); accepted_versions_.insert("test"); } redirected_versions_.clear(); const config::child_list& redirects = cfg_.get_children("redirect"); for (config::child_list::const_iterator i = redirects.begin(); i != redirects.end(); ++i) { const std::vector versions(utils::split((**i)["version"])); for (std::vector::const_iterator j = versions.begin(); j != versions.end(); ++j) { redirected_versions_[*j] = **i; } } proxy_versions_.clear(); const config::child_list& proxies = cfg_.get_children("proxy"); for (config::child_list::const_iterator p = proxies.begin(); p != proxies.end(); ++p) { const std::vector versions(utils::split((**p)["version"])); for (std::vector::const_iterator j = versions.begin(); j != versions.end(); ++j) { proxy_versions_[*j] = **p; } } ban_manager_.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(); #ifdef HAVE_MYSQLPP if (const config* user_handler = cfg_.child("user_handler")) { user_handler_.reset(new fuh(*user_handler)); // Initiate the mailer class with the [mail] tag // from the config file if (user_handler_) user_handler_->init_mailer(cfg_.child("mail")); } #endif } 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_; } bool server::is_ip_banned(const std::string& ip) const { 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() << "\tnumber_of_ghost_users = " << ghost_players_.size() << "\tlobby_users = " << lobby_.nobservers() << "\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 ntime::source::get_source().start_frame(); try { // We are going to waith 10 seconds before shutting down so users can get out of game. if (graceful_restart && games_.size() == 0 && ++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 automaticaly. // 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)) { process_command(admin_cmd, "*socket*"); } time_t now = time(NULL); if (last_ping_ + 30 <= 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); } // 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=\"" << lexical_cast(now) << "\"" ; simple_wml::document ping( strstr.str().c_str(), simple_wml::INIT_COMPRESSED ) ; simple_wml::string_span s = ping.output_compressed() ; for (wesnothd::player_map::const_iterator i = ghost_players_.begin(); i != ghost_players_.end(); ++i) { DBG_SERVER << "Pinging " << i->second.name() << "(" << i->first << ").\n" ; network::send_raw_data( s.begin(), s.size(), i->first, "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() ; std::copy( players_.begin(), players_.end(), std::inserter( ghost_players_, ghost_players_.begin() ) ) ; } last_ping_ = now; } network::process_send_queue(); network::connection sock = network::accept_connection(); if (sock) { const std::string& ip = network::ip_address(sock); if (is_ip_banned(ip)) { LOG_SERVER << ip << "\trejected banned user.\n"; send_error(sock, "You are banned."); 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_.add_player(sock, true); } } static int sample_counter = 0; std::vector buf; network::bandwidth_in_ptr bandwidth_type; while ((sock = network::receive_data(buf, &bandwidth_type)) != network::null_connection) { metrics_.service_request(); if(buf.empty()) { std::cerr << "received empty packet\n"; continue; } //TODO: this is a HUGE HACK. There was a bug in Wesnoth 1.4 //that caused it to still use binary WML for the leave game //message. (ugh). We will see if this looks like binary WML //and if it does, assume it's a leave_game message if(buf.front() < 5) { std::cerr << "hit wesnoth bug...forcing disconnection incorrectly." << buf.front() << std::endl ; static simple_wml::document leave_game_doc( "[leave_game]\n[/leave_game]\n", simple_wml::INIT_COMPRESSED); process_data(sock, leave_game_doc); 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()); simple_wml::document data(compressed_buf); data.take_ownership_of_buffer(buf_ptr); std::vector().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(config::error& e) { WRN_CONFIG << "Warning: error in received data: " << e.message << "\n"; } catch(simple_wml::error& e) { WRN_CONFIG << "Warning: error in received data\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()) { if (not_logged_in_.is_observer(e.socket)) { DBG_SERVER << ip << "\tNot logged in user disconnected.\n"; not_logged_in_.remove_player(e.socket); } 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)) { 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 (lobby_.is_member(e.socket)) { lobby_.remove_player(e.socket); LOG_SERVER << ip << "\t" << pl_it->second.name() << "\thas logged off. (socket: " << e.socket << ")\n"; } else { for (std::vector::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; } } players_.erase(pl_it); if (lan_server_) { last_user_seen_time_ = time(0); } e.disconnect(); DBG_SERVER << "done closing socket...\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. const wesnothd::player_map::const_iterator pl = ghost_players_.find( sock ) ; if( pl != ghost_players_.end() && 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_.is_observer(sock)) { // 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 (lobby_.is_observer(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::set::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::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, true, "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"], lexical_cast_default(config_it->second["port"],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, true, "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."); 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, "This username contains invalid " "characters. Only alpha-numeric characters, underscores and hyphens" "are allowed."); return; } if (username.size() > 18) { send_error(sock, "This username is too long. Usernames must be 18 characers or less."); return; } // Check if the username is allowed. for (std::vector::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 you chose is reserved and cannot be used by players"); 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).c_str()); } 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) { send_error(sock, "The username you chose is already taken."); return; } } // 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(); if(user_handler_->user_exists(username)) { // This name is registered and no password provided if(password.empty()) { send_password_request(sock, ("The username '" + username + "' is registered on this server.").c_str(), username); 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); } // 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 username '" + username + "' was incorrect.").c_str(), username); LOG_SERVER << network::ip_address(sock) << "\t" << "Login attempt with incorrect password for username '" << 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); } } // Check if the version is now avaliable. 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 player new_player(username, player_cfg, registered, default_max_messages_, default_time_period_, selective_ping ); // 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::pair(sock, new_player)); if( !selective_ping ) ghost_players_.insert( std::pair(sock, new_player) ) ; not_logged_in_.remove_player(sock); lobby_.add_player(sock, true); // Send the new player the entire list of games and players send_doc(games_and_users_list_, sock); if (motd_ != "") { lobby_.send_server_message(motd_.c_str(), 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); lobby_.send_data(diff, sock); LOG_SERVER << network::ip_address(sock) << "\t" << username << "\thas logged on. (socket: " << sock << ")\n"; for (std::vector::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").c_str()); } // Log the IP ip_log_[username] = network::ip_address(sock); // Remove the oldes entry in the size of the IP log exceeds the maximum size if(ip_log_.size() > max_ip_log_size_) ip_log_.erase(ip_log_.begin()); } void server::process_query(const network::connection sock, simple_wml::node& query) { const wesnothd::player_map::const_iterator pl = players_.find(sock); if (pl == players_.end()) { DBG_SERVER << "ERROR: process_query(): Could not find player with socket: " << sock << "\n"; return; } const simple_wml::string_span& command(query["type"]); std::ostringstream response; const std::string& help_msg = "Available commands are: help, games, metrics," " motd, netstats [all], stats, status, wml."; if (admins_.count(sock) != 0) { LOG_SERVER << "Admin Command:" << "\ttype: " << command << "\tIP: "<< network::ip_address(sock) << "\tnick: "<< pl->second.name() << std::endl; response << process_command(command.to_string(), pl->second.name()); // Commands a player may issue. } else if (command == "help") { response << help_msg; } else if (command == "status") { response << process_command(command.to_string() + " " + pl->second.name(), pl->second.name()); } else if (command == "games" || command == "metrics" || command == "motd" || command == "netstats" || command == "netstats all" || command == "sample" || command == "stats" || command == "status " + pl->second.name() || command == "wml") { response << process_command(command.to_string(), pl->second.name()); } else if (command == admin_passwd_) { LOG_SERVER << "New Admin recognized:" << "\tIP: " << network::ip_address(sock) << "\tnick: " << pl->second.name() << std::endl; admins_.insert(sock); response << "You are now recognized as an administrator."; } else if (admin_passwd_.empty() == false) { WRN_SERVER << "FAILED Admin attempt: '" << command << "'\tIP: " << network::ip_address(sock) << "\tnick: " << pl->second.name() << std::endl; response << "Error: unrecognized query: '" << command << "'\n" << help_msg; } else { response << "Error: unrecognized query: '" << command << "'\n" << help_msg; } lobby_.send_server_message(response.str().c_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 std::system(restart_command.c_str()); LOG_SERVER << "New server started with command: " << restart_command << "\n"; } std::string server::process_command(const std::string& query, const std::string& issuer_name) { std::ostringstream out; const std::string::const_iterator i = std::find(query.begin(),query.end(),' '); const std::string command(query.begin(),i); std::string parameters = (i == query.end() ? "" : std::string(i+1,query.end())); utils::strip(parameters); const std::string& help_msg = "Available commands are: ban [