diff --git a/src/server/server.cpp b/src/server/server.cpp index 5c762e9b53d7..bcb8e42dbb7c 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -595,179 +595,193 @@ void server::login(socket_ptr socket) void server::handle_login(socket_ptr socket, std::shared_ptr doc) { if(const simple_wml::node* const login = doc->child("login")) { - // 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)) { - async_send_error(socket, "The nickname '" + username + "' contains invalid " - "characters. Only alpha-numeric characters, underscores and hyphens" - "are allowed.", MP_INVALID_CHARS_IN_NAME_ERROR); - server::login(socket); - return; - } - if (username.size() > 20) { - async_send_error(socket, "The nickname '" + username + "' is too long. Nicks must be 20 characters or less.", - MP_NAME_TOO_LONG_ERROR); - server::login(socket); - return; + if(!is_login_allowed(socket, login)) { + server::login(socket); // keep reading logins from client until we get a successful one } - // Check if the username is allowed. - for (std::vector::const_iterator d_it = disallowed_names_.begin(); - d_it != disallowed_names_.end(); ++d_it) + } else { + async_send_error(socket, "You must login first.", MP_MUST_LOGIN); + } +} + +bool server::is_login_allowed(socket_ptr socket, const simple_wml::node* const login) +{ + // 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)) { + async_send_error(socket, "The nickname '" + username + "' contains invalid " + "characters. Only alpha-numeric characters, underscores and hyphens" + "are allowed.", MP_INVALID_CHARS_IN_NAME_ERROR); + return false; + } + if (username.size() > 20) { + async_send_error(socket, "The nickname '" + username + "' is too long. Nicks must be 20 characters or less.", + MP_NAME_TOO_LONG_ERROR); + return false; + } + // 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(utf8::lowercase(username), + utf8::lowercase(*d_it))) { - if (utils::wildcard_string_match(utf8::lowercase(username), - utf8::lowercase(*d_it))) - { - async_send_error(socket, "The nickname '" + username + "' is reserved and cannot be used by players", - MP_NAME_RESERVED_ERROR); - server::login(socket); - return; - } + async_send_error(socket, "The nickname '" + username + "' is reserved and cannot be used by players", + MP_NAME_RESERVED_ERROR); + return false; } + } - // Check the username isn't already taken - auto p = player_connections_.get().find(username); - bool name_taken = p != player_connections_.get().end(); - - // Check for password - - // Current login procedure for registered nicks is: - // - Client asks to log in with a particular nick - // - Server sends client random nonce 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 nonce - // - Server received password hash hashed with the nonce, - // applies the nonce to the valid hash 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)) { - async_send_warning(socket, "The nickname '" + username + "' is inactive. You cannot claim ownership of this " - "nickname 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(!name_taken) { - send_password_request(socket, "The nickname '" + username +"' is registered on this server.", - username, MP_PASSWORD_REQUEST); - } else { - send_password_request(socket, "The nickname '" + 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; - } + // Check the username isn't already taken + auto p = player_connections_.get().find(username); + bool name_taken = p != player_connections_.get().end(); - // A password (or hashed password) was provided, however - // there is no seed - if(seeds_[reinterpret_cast(socket.get())].empty()) { - send_password_request(socket, "Please try again.", username, MP_NO_SEED_ERROR); - return; - } - // This name is registered and an incorrect password provided - else if(!(user_handler_->login(username, password, seeds_[reinterpret_cast(socket.get())]))) { - const time_t now = time(nullptr); + // Check for password - // Reset the random seed - seeds_.erase(reinterpret_cast(socket.get())); + bool registered; + if(!authenticate(socket, username, (*login)["password"].to_string(), name_taken, registered)) + return true; // it's a failed login but we don't want to call server::login again + // because send_password_request() will handle the next network write and read instead - login_log login_ip = login_log(client_address(socket), 0, now); - std::deque::iterator i = std::find(failed_logins_.begin(), failed_logins_.end(), login_ip); - if(i == failed_logins_.end()) { - failed_logins_.push_back(login_ip); - i = --failed_logins_.end(); + // If we disallow unregistered users and this user is not registered send an error + if(user_handler_ && !registered && deny_unregistered_login_) { + async_send_error(socket, "The nickname '" + username + "' is not registered. " + "This server disallows unregistered nicknames.", MP_NAME_UNREGISTERED_ERROR); + return false; + } - // Remove oldest entry if maximum size is exceeded - if(failed_logins_.size() > failed_login_buffer_size_) - failed_logins_.pop_front(); + if(name_taken) { + if(registered) { + // If there is already a client using this username kick it + process_command("kick " + p->info().name() + " autokick by registered user", username); + } else { + async_send_error(socket, "The nickname '" + username + "' is already taken.", MP_NAME_TAKEN_ERROR); + return false; + } + } - } + simple_wml::node& player_cfg = games_and_users_list_.root().add_child("user"); + async_send_doc(socket, join_lobby_response_, + std::bind(&server::add_player, this, _1, + wesnothd::player(username, player_cfg, registered, + default_max_messages_, default_time_period_, + user_handler_ && user_handler_->user_is_moderator(username)) + ) + ); + LOG_SERVER << client_address(socket) << "\t" << username + << "\thas logged on" << (registered ? " to a registered account" : "") << "\n"; - if (i->first_attempt + failed_login_ban_ < now) { - // Clear and move to the beginning - failed_logins_.erase(i); - failed_logins_.push_back(login_ip); - i = --failed_logins_.end(); - } + if(user_handler_ && user_handler_->user_is_moderator(username)) { + LOG_SERVER << "Admin automatically recognized: IP: " + << client_address(socket) << "\tnick: " + << username << std::endl; + // This string is parsed by the client! + send_server_message(socket, "You are now recognized as an administrator. " + "If you no longer want to be automatically authenticated use '/query signout'."); + } - i->attempts++; + // Log the IP + connection_log ip_name = connection_log(username, client_address(socket), 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(); + } - if (i->attempts > failed_login_limit_) { - LOG_SERVER << ban_manager_.ban(login_ip.ip, now + failed_login_ban_, "Maximum login attempts exceeded", "automatic", "", username); - async_send_error(socket, "You have made too many failed login attempts.", MP_TOO_MANY_ATTEMPTS_ERROR); - } else { - send_password_request(socket, "The password you provided for the nickname '" + username + - "' was incorrect.", username, MP_INCORRECT_PASSWORD_ERROR); - } + return true; +} - // Log the failure - LOG_SERVER << client_address(socket) << "\t" - << "Login attempt with incorrect password for nickname '" << username << "'.\n"; - return; +bool server::authenticate(socket_ptr socket, const std::string& username, const std::string& password, bool name_taken, bool& registered) +{ + // Current login procedure for registered nicks is: + // - Client asks to log in with a particular nick + // - Server sends client random nonce 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 nonce + // - Server received password hash hashed with the nonce, + // applies the nonce to the valid hash and compares the results + + registered = false; + if(user_handler_) { + 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)) { + async_send_warning(socket, "The nickname '" + username + "' is inactive. You cannot claim ownership of this " + "nickname 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(!name_taken) { + send_password_request(socket, "The nickname '" + username +"' is registered on this server.", + username, MP_PASSWORD_REQUEST); + } else { + send_password_request(socket, "The nickname '" + 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); } - // This name exists and the password was neither empty nor incorrect - registered = true; - // Reset the random seed - seeds_.erase(reinterpret_cast(socket.get())); - user_handler_->user_logged_in(username); + return false; } - } - // If we disallow unregistered users and this user is not registered send an error - if(user_handler_ && !registered && deny_unregistered_login_) { - async_send_error(socket, "The nickname '" + username + "' is not registered. " - "This server disallows unregistered nicknames.", MP_NAME_UNREGISTERED_ERROR); - return; - } - - if(name_taken) { - if(registered) { - // If there is already a client using this username kick it - process_command("kick " + p->info().name() + " autokick by registered user", username); - } else { - async_send_error(socket, "The nickname '" + username + "' is already taken.", MP_NAME_TAKEN_ERROR); - server::login(socket); - return; + // A password (or hashed password) was provided, however + // there is no seed + if(seeds_[reinterpret_cast(socket.get())].empty()) { + send_password_request(socket, "Please try again.", username, MP_NO_SEED_ERROR); + return false; } - } + // This name is registered and an incorrect password provided + else if(!(user_handler_->login(username, password, seeds_[reinterpret_cast(socket.get())]))) { + const time_t now = time(nullptr); - simple_wml::node& player_cfg = games_and_users_list_.root().add_child("user"); - async_send_doc(socket, join_lobby_response_, - std::bind(&server::add_player, this, _1, - wesnothd::player(username, player_cfg, registered, - default_max_messages_, default_time_period_, - user_handler_ && user_handler_->user_is_moderator(username)) - ) - ); - LOG_SERVER << client_address(socket) << "\t" << username - << "\thas logged on" << (registered ? " to a registered account" : "") << "\n"; + // Reset the random seed + seeds_.erase(reinterpret_cast(socket.get())); - if(user_handler_ && user_handler_->user_is_moderator(username)) { - LOG_SERVER << "Admin automatically recognized: IP: " - << client_address(socket) << "\tnick: " - << username << std::endl; - // This string is parsed by the client! - send_server_message(socket, "You are now recognized as an administrator. " - "If you no longer want to be automatically authenticated use '/query signout'."); - } + login_log login_ip = login_log(client_address(socket), 0, now); + std::deque::iterator i = std::find(failed_logins_.begin(), failed_logins_.end(), login_ip); + if(i == failed_logins_.end()) { + failed_logins_.push_back(login_ip); + i = --failed_logins_.end(); + + // Remove oldest entry if maximum size is exceeded + if(failed_logins_.size() > failed_login_buffer_size_) + failed_logins_.pop_front(); + + } + + if (i->first_attempt + failed_login_ban_ < now) { + // Clear and move to the beginning + failed_logins_.erase(i); + failed_logins_.push_back(login_ip); + i = --failed_logins_.end(); + } + + i->attempts++; - // Log the IP - connection_log ip_name = connection_log(username, client_address(socket), 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(); + if (i->attempts > failed_login_limit_) { + LOG_SERVER << ban_manager_.ban(login_ip.ip, now + failed_login_ban_, "Maximum login attempts exceeded", "automatic", "", username); + async_send_error(socket, "You have made too many failed login attempts.", MP_TOO_MANY_ATTEMPTS_ERROR); + } else { + send_password_request(socket, "The password you provided for the nickname '" + username + + "' was incorrect.", username, MP_INCORRECT_PASSWORD_ERROR); + } + + // Log the failure + LOG_SERVER << client_address(socket) << "\t" + << "Login attempt with incorrect password for nickname '" << username << "'.\n"; + return false; + } + // This name exists and the password was neither empty nor incorrect + registered = true; + // Reset the random seed + seeds_.erase(reinterpret_cast(socket.get())); + user_handler_->user_logged_in(username); } - } else { - async_send_error(socket, "You must login first.", MP_MUST_LOGIN); } + + return true; } void server::send_password_request(socket_ptr socket, const std::string& msg, diff --git a/src/server/server.hpp b/src/server/server.hpp index aaee2d1233d9..de2689676721 100644 --- a/src/server/server.hpp +++ b/src/server/server.hpp @@ -43,6 +43,8 @@ class server : public server_base void login(socket_ptr socket); void handle_login(socket_ptr socket, std::shared_ptr doc); + bool is_login_allowed(socket_ptr socket, const simple_wml::node* const login); + bool authenticate(socket_ptr socket, const std::string& username, const std::string& password, bool name_taken, bool& registered); void send_password_request(socket_ptr socket, const std::string& msg, const std::string& user, const char* error_code = "", bool force_confirmation = false); bool accepting_connections() const { return !graceful_restart; }