wesnoth/src/multiplayer.cpp
Thomas Baumhauer f34034cc42 made password reminder requests work again...
...(thanks to zookeeper for finding this bug)
2009-01-31 10:57:29 +00:00

611 lines
16 KiB
C++

/* $Id$ */
/*
Copyright (C) 2007 - 2009
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.
*/
#include "global.hpp"
#include "dialogs.hpp"
#include "gettext.hpp"
#include "gui/dialogs/mp_connect.hpp"
#include "gui/dialogs/mp_create_game.hpp"
#include "gui/widgets/window.hpp"
#include "md5.hpp"
#include "multiplayer.hpp"
#include "multiplayer_connect.hpp"
#include "multiplayer_wait.hpp"
#include "multiplayer_lobby.hpp"
#include "playmp_controller.hpp"
#include "playcampaign.hpp"
#include "upload_log.hpp"
#include "formula_string_utils.hpp"
#define LOG_NW LOG_STREAM(info, network)
namespace {
class network_game_manager
{
public:
// Add a constructor to avoid stupid warnings with some versions of GCC
network_game_manager() {};
~network_game_manager()
{
if(network::nconnections() > 0) {
LOG_NW << "sending leave_game\n";
config cfg;
cfg.add_child("leave_game");
network::send_data(cfg, 0, true);
LOG_NW << "sent leave_game\n";
}
};
};
}
static void run_lobby_loop(display& disp, mp::ui& ui)
{
disp.video().modeChanged();
bool first = true;
font::cache_mode(font::CACHE_LOBBY);
while (ui.get_result() == mp::ui::CONTINUE) {
if (disp.video().modeChanged() || first) {
SDL_Rect lobby_pos = { 0, 0, disp.video().getx(), disp.video().gety() };
ui.set_location(lobby_pos);
first = false;
}
events::pump();
events::raise_process_event();
events::raise_draw_event();
ui.process_network();
disp.flip();
disp.delay(20);
}
font::cache_mode(font::CACHE_GAME);
}
namespace {
enum server_type {
ABORT_SERVER,
WESNOTHD_SERVER,
SIMPLE_SERVER
};
}
static server_type open_connection(game_display& disp, const std::string& original_host)
{
std::string h = original_host;
if(h.empty()) {
gui2::tmp_connect dlg;
dlg.show(disp.video());
if(dlg.get_retval() == gui2::twindow::OK) {
h = preferences::network_host();
} else {
return ABORT_SERVER;
}
}
network::connection sock;
const int pos = h.find_first_of(":");
std::string host;
unsigned int port;
if(pos == -1) {
host = h;
port = 15000;
} else {
host = h.substr(0, pos);
port = lexical_cast_default<unsigned int>(h.substr(pos + 1), 15000);
}
// shown_hosts is used to prevent the client being locked in a redirect
// loop.
typedef std::pair<std::string, int> hostpair;
std::set<hostpair> shown_hosts;
shown_hosts.insert(hostpair(host, port));
config::child_list redirects;
config data;
sock = dialogs::network_connect_dialog(disp,_("Connecting to Server..."),host,port);
do {
if (!sock) {
return ABORT_SERVER;
}
data.clear();
network::connection data_res = dialogs::network_receive_dialog(
disp,_("Reading from Server..."),data);
if (!data_res) return ABORT_SERVER;
mp::check_response(data_res, data);
// Backwards-compatibility "version" attribute
const std::string& version = data["version"];
if(version.empty() == false && version != game_config::version) {
utils::string_map i18n_symbols;
i18n_symbols["version1"] = version;
i18n_symbols["version2"] = game_config::version;
const std::string errorstring = vgettext("The server requires version '$version1' while you are using version '$version2'", i18n_symbols);
throw network::error(errorstring);
}
// Check for "redirect" messages
if(data.child("redirect")) {
config* redirect = data.child("redirect");
host = (*redirect)["host"];
port = lexical_cast_default<unsigned int>((*redirect)["port"], 15000);
if(shown_hosts.find(hostpair(host,port)) != shown_hosts.end()) {
throw network::error(_("Server-side redirect loop"));
}
shown_hosts.insert(hostpair(host, port));
if(network::nconnections() > 0)
network::disconnect();
sock = dialogs::network_connect_dialog(disp,_("Connecting to Server..."),host,port);
continue;
}
if(data.child("version")) {
config cfg;
config res;
cfg["version"] = game_config::version;
res.add_child("version", cfg);
network::send_data(res, 0, true);
}
//if we got a direction to login
if(data.child("mustlogin")) {
for(;;) {
std::string password_reminder = "";
std::string login = preferences::login();
config response ;
config &sp = response.add_child("login") ;
sp["username"] = login ;
// Login and enable selective pings -- saves server bandwidth
// If ping_timeout has a non-zero value, do not enable
// selective pings as this will cause clients to falsely
// believe the server has died and disconnect.
if( preferences::get_ping_timeout() ) {
// Pings required so disable selective pings
sp["selective_ping"] = "0" ;
} else {
// Client is bandwidth friendly so allow
// server to optimize ping frequency as needed.
sp["selective_ping"] = "1" ;
}
network::send_data(response, 0, true);
// Get response for our login request...
network::connection data_res = network::receive_data(data, 0, 3000);
if(!data_res) {
throw network::error(_("Connection timed out"));
}
config* error = data.child("error");
// ... and get us out of here if the server did not complain
if(!error) break;
do {
std::string password = preferences::password();
const bool is_pw_request = !((*error)["password_request"].empty()) && !(password.empty());
// If the server asks for a password, provide one if we can
// or request a password reminder.
// Otherwise go directly to the username/password dialog
if(is_pw_request || !password_reminder.empty()) {
if(is_pw_request) {
if((*error)["phpbb_encryption"] == "yes") {
// start the hashing
std::string result;
// Check if we have everything we need
if((*error)["salt"].empty() || (*error)["hash_seed"].empty()) {
return ABORT_SERVER;
}
std::string itoa64("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
std::string salt = (*error)["salt"];
int hash_seed;
try {
hash_seed = lexical_cast_default<int>((*error)["hash_seed"]);
} catch (bad_lexical_cast) {
std::cerr << "Bad lexical cast reading hash_seed\n";
return ABORT_SERVER;
}
// Start the MD5 hashing
salt.append(password);
MD5 md5_worker;
md5_worker.update((unsigned char *)salt.c_str(),salt.length());
md5_worker.finalize();
unsigned char * output = (unsigned char *) malloc (sizeof(unsigned char) * 16);
output = md5_worker.raw_digest();
std::string temp_hash;
do {
temp_hash = std::string((char *) output, (char *) output + 16);
temp_hash.append(password);
MD5 md5_worker;
md5_worker.update((unsigned char *)temp_hash.c_str(),temp_hash.length());
md5_worker.finalize();
output = md5_worker.raw_digest();
} while (--hash_seed);
temp_hash = std::string((char *) output, (char *) output + 16);
// Now encode the resulting mix
std::string encoded_hash;
unsigned int i = 0, value;
do {
value = output[i++];
encoded_hash.append(itoa64.substr(value & 0x3f,1));
if(i < 16)
value |= (int)output[i] << 8;
encoded_hash.append(itoa64.substr((value >> 6) & 0x3f,1));
if(i++ >= 16)
break;
if(i < 16)
value |= (int)output[i] << 16;
encoded_hash.append(itoa64.substr((value >> 12) & 0x3f,1));
if(i++ >= 16)
break;
encoded_hash.append(itoa64.substr((value >> 18) & 0x3f,1));
} while (i < 16);
free (output);
// Now mix the resulting hash with the random seed
result = encoded_hash + (*error)["random_salt"];
MD5 md5_worker2;
md5_worker2.update((unsigned char *)result.c_str(), result.size());
md5_worker2.finalize();
result = std::string(md5_worker2.hex_digest());
sp["password"] = result;
} else {
sp["password"] = password;
}
}
sp["password_reminder"] = password_reminder;
// Once again send our request...
network::send_data(response, 0, true);
network::connection data_res = network::receive_data(data, 0, 3000);
if(!data_res) {
throw network::error(_("Connection timed out"));
}
error = data.child("error");
// ... and get us out of here if the server is happy now
if(!error) break;
}
password_reminder = "";
// Providing a password either was not attempted because we did not
// have any or failed:
// Now show a dialog that displays the error and allows to
// enter a new user name and/or password
gui2::tmp_login dlg((*error)["message"],
!((*error)["password_request"].empty()));
dlg.show(disp.video());
switch(dlg.get_retval()) {
//Log in with password
case gui2::twindow::OK:
break;
//Request a password reminder
case 1:
password_reminder = "yes";
break;
// Cancel
default: return ABORT_SERVER;
}
// If we have got a new username we have to start all over again
} while(login == preferences::login());
// Somewhat hacky...
// If we broke out of the do-while loop above error
// is still going to be NULL
if(!error) break;
} // end login loop
}
} while(!(data.child("join_lobby") || data.child("join_game")));
if (h != preferences::server_list().front().address)
preferences::set_network_host(h);
if (data.child("join_lobby")) {
return WESNOTHD_SERVER;
} else {
return SIMPLE_SERVER;
}
}
// The multiplayer logic consists in 4 screens:
//
// lobby <-> create <-> connect <-> (game)
// <------------> wait <-> (game)
//
// To each of this screen corresponds a dialog, each dialog being defined in
// the multiplayer_(screen) file.
//
// The functions enter_(screen)_mode are simple functions that take care of
// creating the dialogs, then, according to the dialog result, of calling other
// of those screen functions.
static void enter_wait_mode(game_display& disp, const config& game_config, mp::chat& chat, config& gamelist, bool observe)
{
mp::ui::result res;
game_state state;
network_game_manager m;
upload_log nolog(false);
gamelist.clear();
statistics::fresh_stats();
{
mp::wait ui(disp, game_config, chat, gamelist);
ui.join_game(observe);
run_lobby_loop(disp, ui);
res = ui.get_result();
if (res == mp::ui::PLAY) {
ui.start_game();
// FIXME commented a pointeless if since the else does exactly the same thing
//if (preferences::skip_mp_replay()){
//FIXME implement true skip replay
//state = ui.request_snapshot();
//state = ui.get_state();
//}
//else{
state = ui.get_state();
//}
}
}
switch (res) {
case mp::ui::PLAY:
play_game(disp, state, game_config, nolog, IO_CLIENT,
preferences::skip_mp_replay() && observe);
recorder.clear();
break;
case mp::ui::QUIT:
default:
break;
}
}
static void enter_create_mode(game_display& disp, const config& game_config, mp::chat& chat, config& gamelist, mp::controller default_controller, bool local_players_only = false);
static void enter_connect_mode(game_display& disp, const config& game_config,
mp::chat& chat, config& gamelist, const mp::create::parameters& params,
mp::controller default_controller, bool local_players_only = false)
{
mp::ui::result res;
game_state state;
const network::manager net_manager(1,1);
network_game_manager m;
upload_log nolog(false);
gamelist.clear();
statistics::fresh_stats();
{
mp::connect ui(disp, game_config, chat, gamelist, params, default_controller, local_players_only);
run_lobby_loop(disp, ui);
res = ui.get_result();
// start_game() updates the parameters to reflect game start,
// so it must be called before get_level()
if (res == mp::ui::PLAY) {
ui.start_game();
state = ui.get_state();
}
}
switch (res) {
case mp::ui::PLAY:
play_game(disp, state, game_config, nolog, IO_SERVER);
recorder.clear();
break;
case mp::ui::CREATE:
enter_create_mode(disp, game_config, chat, gamelist, default_controller, local_players_only);
break;
case mp::ui::QUIT:
default:
network::send_data(config("refresh_lobby"), 0, true);
break;
}
}
static void enter_create_mode(game_display& disp, const config& game_config, mp::chat& chat, config& gamelist, mp::controller default_controller, bool local_players_only)
{
if(gui2::new_widgets) {
gui2::tmp_create_game dlg(game_config);
dlg.show(disp.video());
network::send_data(config("refresh_lobby"), 0, true);
} else {
mp::ui::result res;
mp::create::parameters params;
{
mp::create ui(disp, game_config, chat, gamelist);
run_lobby_loop(disp, ui);
res = ui.get_result();
params = ui.get_parameters();
}
switch (res) {
case mp::ui::CREATE:
enter_connect_mode(disp, game_config, chat, gamelist, params, default_controller, local_players_only);
break;
case mp::ui::QUIT:
default:
//update lobby content
network::send_data(config("refresh_lobby"), 0, true);
break;
}
}
}
static void enter_lobby_mode(game_display& disp, const config& game_config, mp::chat& chat, config& gamelist)
{
mp::ui::result res;
while (true) {
{
mp::lobby ui(disp, game_config, chat, gamelist);
run_lobby_loop(disp, ui);
res = ui.get_result();
}
switch (res) {
case mp::ui::JOIN:
try {
enter_wait_mode(disp, game_config, chat, gamelist, false);
} catch(config::error& error) {
if(!error.message.empty()) {
gui::show_error_message(disp, error.message);
}
//update lobby content
network::send_data(config("refresh_lobby"), 0, true);
}
break;
case mp::ui::OBSERVE:
try {
enter_wait_mode(disp, game_config, chat, gamelist, true);
} catch(config::error& error) {
if(!error.message.empty()) {
gui::show_error_message(disp, error.message);
}
}
//update lobby content
network::send_data(config("refresh_lobby"), 0, true);
break;
case mp::ui::CREATE:
try {
enter_create_mode(disp, game_config, chat, gamelist, mp::CNTR_NETWORK);
} catch(config::error& error) {
if (!error.message.empty())
gui::show_error_message(disp, error.message);
//update lobby content
network::send_data(config("refresh_lobby"), 0, true);
}
break;
case mp::ui::QUIT:
return;
case mp::ui::PREFERENCES:
{
const preferences::display_manager disp_manager(&disp);
preferences::show_preferences_dialog(disp,game_config);
//update lobby content
network::send_data(config("refresh_lobby"), 0, true);
}
break;
default:
return;
}
}
}
namespace mp {
void start_local_game(game_display& disp, const config& game_config,
mp::controller default_controller)
{
const rand_rng::set_random_generator generator_setter(&recorder);
mp::chat chat;
config gamelist;
playmp_controller::set_replay_last_turn(0);
preferences::set_message_private(false);
enter_create_mode(disp, game_config, chat, gamelist, default_controller, true);
}
void start_client(game_display& disp, const config& game_config,
const std::string host)
{
const rand_rng::set_random_generator generator_setter(&recorder);
const network::manager net_manager(1,1);
authenticated = false;
mp::chat chat;
config gamelist;
server_type type = open_connection(disp, host);
switch(type) {
case WESNOTHD_SERVER:
enter_lobby_mode(disp, game_config, chat, gamelist);
break;
case SIMPLE_SERVER:
playmp_controller::set_replay_last_turn(0);
preferences::set_message_private(false);
enter_wait_mode(disp, game_config, chat, gamelist, false);
break;
case ABORT_SERVER:
break;
}
}
bool authenticated;
void admin_authentication(const std::string& sender, const std::string& message) {
if(sender != "server") return;
if(message.find("You are now recognized as an administrator.") == 0) {
authenticated = true;
} else if(message.find("You are no longer recognized as an administrator.") == 0) {
authenticated = false;
}
}
}