1 /*
2  * Copyright (C) 2004-2020 by the Widelands Development Team
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17  *
18  */
19 
20 #include "network/internet_gaming.h"
21 
22 #include <algorithm>
23 #include <memory>
24 
25 #include <boost/algorithm/string.hpp>
26 #include <boost/format.hpp>
27 
28 #include "base/i18n.h"
29 #include "base/log.h"
30 #include "base/random.h"
31 #include "base/warning.h"
32 #include "build_info.h"
33 #include "io/fileread.h"
34 #include "io/filesystem/layered_filesystem.h"
35 #include "network/crypto.h"
36 #include "network/internet_gaming_messages.h"
37 #include "network/internet_gaming_protocol.h"
38 
39 /// Private constructor by purpose: NEVER call directly. Always call InternetGaming::ref(), this
40 /// will ensure
41 /// that only one instance is running at time.
InternetGaming()42 InternetGaming::InternetGaming()
43    : net(nullptr),
44      state_(OFFLINE),
45      reg_(false),
46      port_(kInternetGamingPort),
47      clientrights_(INTERNET_CLIENT_UNREGISTERED),
48      gameips_(),
49      clientupdateonmetaserver_(true),
50      gameupdateonmetaserver_(true),
51      clientupdate_(false),
52      gameupdate_(false),
53      time_offset_(0),
54      waittimeout_(std::numeric_limits<int32_t>::max()),
55      lastping_(time(nullptr)) {
56 	// Fill the list of possible messages from the server
57 	InternetGamingMessages::fill_map();
58 
59 	// Set connection tracking variables to 0
60 	lastbrokensocket_[0] = 0;
61 	lastbrokensocket_[1] = 0;
62 }
63 
64 /// resets all stored variables without the chat messages for a clean new login (not relogin)
reset()65 void InternetGaming::reset() {
66 	net.reset();
67 	state_ = OFFLINE;
68 	authenticator_ = "";
69 	reg_ = false;
70 	meta_ = INTERNET_GAMING_METASERVER;
71 	port_ = kInternetGamingPort;
72 	clientname_ = "";
73 	clientrights_ = INTERNET_CLIENT_UNREGISTERED;
74 	gamename_ = "";
75 	gameips_ = std::make_pair(NetAddress(), NetAddress());
76 	clientupdateonmetaserver_ = true;
77 	gameupdateonmetaserver_ = true;
78 	clientupdate_ = false;
79 	gameupdate_ = false;
80 	time_offset_ = 0;
81 	waitcmd_ = "";
82 	waittimeout_ = std::numeric_limits<int32_t>::max();
83 	lastbrokensocket_[0] = 0;
84 	lastbrokensocket_[1] = 0;
85 	lastping_ = time(nullptr);
86 
87 	clientlist_.clear();
88 	gamelist_.clear();
89 }
90 
91 /// the one and only InternetGaming instance.
92 static InternetGaming* ig = nullptr;
93 
94 /// \returns the one and only InternetGaming instance.
ref()95 InternetGaming& InternetGaming::ref() {
96 	if (!ig) {
97 		ig = new InternetGaming();
98 	}
99 	return *ig;
100 }
101 
initialize_connection()102 void InternetGaming::initialize_connection() {
103 	// First of all try to connect to the metaserver
104 	log("InternetGaming: Connecting to the metaserver.\n");
105 	NetAddress addr;
106 	if (NetAddress::resolve_to_v6(&addr, meta_, port_)) {
107 		net = NetClient::connect(addr);
108 	}
109 	if ((!net || !net->is_connected()) && NetAddress::resolve_to_v4(&addr, meta_, port_)) {
110 		net = NetClient::connect(addr);
111 	}
112 	if (!net || !net->is_connected()) {
113 		throw WLWarning(_("Could not establish connection to host"),
114 		                _("Widelands could not establish a connection to the given address.\n"
115 		                  "Either there was no metaserver running at the supposed port or\n"
116 		                  "your network setup is broken."));
117 	}
118 
119 	// Of course not 100% true, but we just care about an answer at all, so we reset this tracker
120 	lastping_ = time(nullptr);
121 }
122 
123 /// Login to metaserver
login(const std::string & nick,const std::string & authenticator,bool registered,const std::string & meta,uint32_t port)124 bool InternetGaming::login(const std::string& nick,
125                            const std::string& authenticator,
126                            bool registered,
127                            const std::string& meta,
128                            uint32_t port) {
129 
130 	// Reset local state. Only resetting on logout() or error isn't enough since
131 	// the game might jump to the main menu from other places, too
132 	reset();
133 
134 	clientname_ = nick;
135 	reg_ = registered;
136 	meta_ = meta;
137 	port_ = port;
138 
139 	if (registered) {
140 		authenticator_ = authenticator;
141 	} else {
142 		authenticator_ = crypto::sha1(nick + authenticator);
143 	}
144 
145 	assert(!authenticator_.empty());
146 
147 	return do_login();
148 }
149 
do_login(bool should_relogin)150 bool InternetGaming::do_login(bool should_relogin) {
151 
152 	initialize_connection();
153 
154 	// If we are here, a connection was established and we can send our login package through the
155 	// socket.
156 	log("InternetGaming: Sending login request.\n");
157 	SendPacket s;
158 	s.string(IGPCMD_LOGIN);
159 	s.string(boost::lexical_cast<std::string>(kInternetGamingProtocolVersion));
160 	s.string(clientname_);
161 	s.string(build_id());
162 	s.string(bool2str(reg_));
163 	s.string(reg_ ? "" : authenticator_);
164 	net->send(s);
165 
166 	// Now let's see, whether the metaserver is answering
167 	uint32_t const secs = time(nullptr);
168 	state_ = CONNECTING;
169 	while (kInternetGamingTimeout > time(nullptr) - secs) {
170 		handle_metaserver_communication();
171 		// Check if we are a step further... if yes handle_packet has taken care about all the
172 		// paperwork, so we put our feet up and just return. ;)
173 		if (state_ != CONNECTING) {
174 			if (state_ == LOBBY) {
175 				if (!should_relogin) {
176 					format_and_add_chat(
177 					   "", "", true, _("Users marked with IRC will possibly not react to messages."));
178 				}
179 
180 				return true;
181 			} else if (error()) {
182 				return false;
183 			}
184 		}
185 	}
186 	log("InternetGaming: No answer from metaserver!\n");
187 	logout("NO_ANSWER");
188 	return false;
189 }
190 
191 /// Relogin to metaserver after loosing connection
relogin()192 bool InternetGaming::relogin() {
193 	if (!error()) {
194 		throw wexception("InternetGaming::relogin: This only makes sense if there was an error.");
195 	}
196 
197 	if (!do_login(true)) {
198 		return false;
199 	}
200 
201 	state_ = LOBBY;
202 	// Client is reconnected, so let's try resend the timeouted command.
203 	if (waitcmd_ == IGPCMD_GAME_CONNECT) {
204 		join_game(gamename_);
205 	} else if (waitcmd_ == IGPCMD_GAME_OPEN) {
206 		state_ = IN_GAME;
207 		open_game();
208 	} else if (waitcmd_ == IGPCMD_GAME_START) {
209 		state_ = IN_GAME;
210 		set_game_playing();
211 	}
212 
213 	log("InternetGaming: Reconnected to metaserver\n");
214 	format_and_add_chat("", "", true, _("Successfully reconnected to the metaserver!"));
215 
216 	return true;
217 }
218 
219 /// logout of the metaserver
220 /// \note \arg msgcode should be a message from the list of InternetGamingMessages
logout(const std::string & msgcode)221 void InternetGaming::logout(const std::string& msgcode) {
222 
223 	// Just in case the metaserver is listening on the socket - tell him we break up with him ;)
224 	if (net && net->is_connected()) {
225 		SendPacket s;
226 		s.string(IGPCMD_DISCONNECT);
227 		s.string(msgcode);
228 		net->send(s);
229 	}
230 
231 	const std::string& msg = InternetGamingMessages::get_message(msgcode);
232 	log("InternetGaming: logout(%s)\n", msg.c_str());
233 	format_and_add_chat("", "", true, msg);
234 
235 	reset();
236 }
237 
check_password(const std::string & nick,const std::string & pwd,const std::string & metaserver,uint32_t port)238 bool InternetGaming::check_password(const std::string& nick,
239                                     const std::string& pwd,
240                                     const std::string& metaserver,
241                                     uint32_t port) {
242 	reset();
243 
244 	meta_ = metaserver;
245 	port_ = port;
246 	initialize_connection();
247 
248 	// Has to be set for the password challenge later on
249 	authenticator_ = pwd;
250 
251 	log("InternetGaming: Verifying password.\n");
252 	{
253 		SendPacket s;
254 		s.string(IGPCMD_CHECK_PWD);
255 		s.string(boost::lexical_cast<std::string>(kInternetGamingProtocolVersion));
256 		s.string(nick);
257 		s.string(build_id());
258 		net->send(s);
259 	}
260 
261 	// Now let's see, whether the metaserver is answering
262 	uint32_t const secs = time(nullptr);
263 	state_ = CONNECTING;
264 	while (kInternetGamingTimeout > time(nullptr) - secs) {
265 		handle_metaserver_communication(false);
266 		if (state_ != CONNECTING) {
267 			if (state_ == LOBBY) {
268 				SendPacket s;
269 				s.string(IGPCMD_DISCONNECT);
270 				s.string("CONNECTION_CLOSED");
271 				net->send(s);
272 				reset();
273 				return true;
274 			} else if (error()) {
275 				reset();
276 				return false;
277 			}
278 		}
279 	}
280 	log("InternetGaming: No answer from metaserver!\n");
281 	reset();
282 	return false;
283 }
284 
285 /**
286  * Handle situation when reading from socket failed.
287  */
handle_failed_read()288 void InternetGaming::handle_failed_read() {
289 	set_error();
290 	const std::string& msg = InternetGamingMessages::get_message("CONNECTION_LOST");
291 	log("InternetGaming: Error: %s\n", msg.c_str());
292 	format_and_add_chat("", "", true, msg);
293 
294 	// Check how much time passed since the socket broke the last time
295 	// Maybe something is completely wrong at the moment?
296 	// At least it seems to be, if the socket broke three times in the last 10 seconds...
297 	time_t now = time(nullptr);
298 	if ((now - lastbrokensocket_[1] < 10) && (now - lastbrokensocket_[0] < 10)) {
299 		reset();
300 		set_error();
301 		return;
302 	}
303 	lastbrokensocket_[1] = lastbrokensocket_[0];
304 	lastbrokensocket_[0] = now;
305 
306 	// Try to relogin
307 	if (!relogin()) {
308 		// Do not try to relogin again automatically.
309 		reset();
310 		set_error();
311 	}
312 }
313 
314 /// handles all communication between the metaserver and the client
handle_metaserver_communication(bool relogin_on_error)315 void InternetGaming::handle_metaserver_communication(bool relogin_on_error) {
316 	if (error()) {
317 		return;
318 	}
319 	try {
320 		while (net != nullptr) {
321 			// Check if the connection is still open
322 			if (!net->is_connected()) {
323 				handle_failed_read();
324 				return;
325 			}
326 			// Process all available packets
327 			std::unique_ptr<RecvPacket> packet = net->try_receive();
328 			if (packet) {
329 				handle_packet(*packet, relogin_on_error);
330 			} else {
331 				// Nothing more to receive
332 				break;
333 			}
334 		}
335 	} catch (const std::exception& e) {
336 		logout((boost::format(_("Something went wrong: %s")) % e.what()).str());
337 		set_error();
338 	}
339 
340 	if (state_ == LOBBY) {
341 		// client is in the lobby and therefore we want realtime information updates
342 		if (clientupdateonmetaserver_) {
343 			SendPacket s;
344 			s.string(IGPCMD_CLIENTS);
345 			net->send(s);
346 
347 			clientupdateonmetaserver_ = false;
348 		}
349 
350 		if (gameupdateonmetaserver_) {
351 			SendPacket s;
352 			s.string(IGPCMD_GAMES);
353 			net->send(s);
354 
355 			gameupdateonmetaserver_ = false;
356 		}
357 	}
358 
359 	if (!waitcmd_.empty()) {
360 		// Check if timeout is reached
361 		time_t now = time(nullptr);
362 		if (now > waittimeout_) {
363 			set_error();
364 			waittimeout_ = std::numeric_limits<int32_t>::max();
365 			log("InternetGaming: reached a timeout for an awaited answer of the metaserver!\n");
366 			if (relogin_on_error && !relogin()) {
367 				// Do not try to relogin again automatically.
368 				reset();
369 				set_error();
370 			}
371 		}
372 	}
373 
374 	// Check connection to the metaserver
375 	// Was a ping received in the last 4 minutes?
376 	if (time(nullptr) - lastping_ > 240) {
377 		// Try to relogin
378 		set_error();
379 		if (relogin_on_error && !relogin()) {
380 			// Do not try to relogin again automatically.
381 			reset();
382 			set_error();
383 		}
384 	}
385 }
386 
387 /// Handle one packet received from the metaserver.
handle_packet(RecvPacket & packet,bool relogin_on_error)388 void InternetGaming::handle_packet(RecvPacket& packet, bool relogin_on_error) {
389 	std::string cmd = packet.string();
390 
391 	// First check if everything is fine or whether the metaserver broke up with the client.
392 	if (cmd == IGPCMD_DISCONNECT) {
393 		std::string reason = packet.string();
394 		format_and_add_chat("", "", true, InternetGamingMessages::get_message(reason));
395 		if (reason == "CLIENT_TIMEOUT") {
396 			// Try to relogin
397 			set_error();
398 			if (relogin_on_error && !relogin()) {
399 				// Do not try to relogin again automatically.
400 				reset();
401 				set_error();
402 			}
403 		}
404 		return;
405 	} else if (cmd == IGPCMD_PING) {
406 		// Client received a PING and should immediately PONG as requested
407 		SendPacket s;
408 		s.string(IGPCMD_PONG);
409 		net->send(s);
410 
411 		lastping_ = time(nullptr);
412 		return;
413 	}
414 
415 	// Are we already online?
416 	if (state_ == CONNECTING) {
417 		if (cmd == IGPCMD_PWD_CHALLENGE) {
418 			const std::string nonce = packet.string();
419 			SendPacket s;
420 			s.string(IGPCMD_PWD_CHALLENGE);
421 			s.string(crypto::sha1(nonce + authenticator_));
422 			net->send(s);
423 			return;
424 
425 		} else if (cmd == IGPCMD_LOGIN) {
426 			// Clients request to login was granted
427 			format_and_add_chat("", "", true, _("Welcome to the Widelands Metaserver!"));
428 			const std::string assigned_name = packet.string();
429 			if (clientname_ != assigned_name) {
430 				format_and_add_chat(
431 				   "", "", true, (boost::format(_("You have been logged in as '%s' since your "
432 				                                  "requested name is already in use or reserved.")) %
433 				                  assigned_name)
434 				                    .str());
435 			}
436 			clientname_ = assigned_name;
437 			clientrights_ = packet.string();
438 			if (reg_ && clientrights_ == INTERNET_CLIENT_UNREGISTERED) {
439 				// Permission downgrade: We logged in with less rights than we wanted to.
440 				// Happens when we are already logged in with another client.
441 				reg_ = false;
442 				authenticator_ = crypto::sha1(clientname_ + authenticator_);
443 			}
444 			format_and_add_chat("", "", true, _("Our forums can be found at:"));
445 			format_and_add_chat("", "", true, "https://www.widelands.org/forum/");
446 			format_and_add_chat("", "", true, _("For reporting bugs, visit:"));
447 			format_and_add_chat("", "", true, "https://www.widelands.org/wiki/ReportingBugs/");
448 			state_ = LOBBY;
449 			// Append UTC time to login message to ease linking between client output and
450 			// metaserver logs. The string returned by asctime is terminated by \n
451 			const time_t now = time(nullptr);
452 			log("InternetGaming: Client %s logged in at UTC %s", clientname_.c_str(),
453 			    asctime(gmtime(&now)));
454 			return;
455 
456 		} else if (cmd == IGPCMD_PWD_OK) {
457 			const time_t now = time(nullptr);
458 			log("InternetGaming: Password check successful at UTC %s", asctime(gmtime(&now)));
459 			state_ = LOBBY;
460 			return;
461 
462 		} else if (cmd == IGPCMD_ERROR) {
463 			std::string errortype = packet.string();
464 			if (errortype != IGPCMD_LOGIN && errortype != IGPCMD_PWD_CHALLENGE) {
465 				log("InternetGaming: Strange ERROR in connecting state: %s\n", errortype.c_str());
466 				throw WLWarning(
467 				   _("Mixed up"), _("The metaserver sent a strange ERROR during connection"));
468 			}
469 			// Clients login request got rejected
470 			logout(packet.string());
471 			set_error();
472 			return;
473 
474 		} else {
475 			logout();
476 			set_error();
477 			log("InternetGaming: Expected a LOGIN, PWD_CHALLENGE or ERROR packet from server, but "
478 			    "received command %s. Maybe the metaserver is using a different protocol version?\n",
479 			    cmd.c_str());
480 			throw WLWarning(
481 			   _("Unexpected packet"),
482 			   _("Received an unexpected network packet from the metaserver. The metaserver could be "
483 			     "using a different protocol version. If the error persists, try updating your "
484 			     "game."));
485 		}
486 	}
487 	try {
488 		if (cmd == IGPCMD_LOGIN) {
489 			// Login specific commands but not in CONNECTING state...
490 			log("InternetGaming: Received %s cmd although client is not in CONNECTING state.\n",
491 			    cmd.c_str());
492 			std::string temp =
493 			   (boost::format(
494 			       _("WARNING: Received a %s command although we are not in CONNECTING state.")) %
495 			    cmd)
496 			      .str();
497 			format_and_add_chat("", "", true, temp);
498 		}
499 
500 		else if (cmd == IGPCMD_TIME) {
501 			// Client received the server time
502 			time_offset_ = boost::lexical_cast<int>(packet.string()) - time(nullptr);
503 			log("InternetGaming: Server time offset is %d second(s).\n", time_offset_);
504 			std::string temp =
505 			   (boost::format(ngettext("Server time offset is %d second.",
506 			                           "Server time offset is %d seconds.", time_offset_)) %
507 			    time_offset_)
508 			      .str();
509 			format_and_add_chat("", "", true, temp);
510 		}
511 
512 		else if (cmd == IGPCMD_CHAT) {
513 			// Client received a chat message
514 			std::string sender = packet.string();
515 			std::string message = packet.string();
516 			std::string type = packet.string();
517 
518 			if (type != "public" && type != "private" && type != "system") {
519 				throw WLWarning(
520 				   _("Invalid message type"), _("Invalid chat message type \"%s\"."), type.c_str());
521 			}
522 
523 			bool personal = type == "private";
524 			bool system = type == "system";
525 
526 			format_and_add_chat(sender, personal ? clientname_ : "", system, message);
527 		}
528 
529 		else if (cmd == IGPCMD_GAMES_UPDATE) {
530 			// Client received a note, that the list of games was changed
531 			log("InternetGaming: Game update on metaserver.\n");
532 			gameupdateonmetaserver_ = true;
533 		}
534 
535 		else if (cmd == IGPCMD_GAMES) {
536 			// Client received the new list of games
537 			uint8_t number = boost::lexical_cast<int>(packet.string()) & 0xff;
538 			std::vector<InternetGame> old = gamelist_;
539 			gamelist_.clear();
540 			log("InternetGaming: Received a game list update with %u items.\n", number);
541 			for (uint8_t i = 0; i < number; ++i) {
542 				InternetGame* ing = new InternetGame();
543 				ing->name = packet.string();
544 				ing->build_id = packet.string();
545 				ing->connectable = packet.string();
546 				gamelist_.push_back(*ing);
547 
548 				bool found = false;
549 				for (InternetGame& old_game : old) {
550 					if (old_game.name == ing->name) {
551 						found = true;
552 						old_game.name = "";
553 						break;
554 					}
555 				}
556 				if (!found && ing->connectable != INTERNET_GAME_RUNNING &&
557 				    (ing->build_id == build_id() || (ing->build_id.compare(0, 6, "build-") != 0 &&
558 				                                     build_id().compare(0, 6, "build-") != 0))) {
559 					format_and_add_chat(
560 					   "", "", true,
561 					   (boost::format(_("The game %s is now available")) % ing->name).str());
562 				}
563 
564 				delete ing;
565 				ing = nullptr;
566 			}
567 
568 			for (InternetGame& old_game : old) {
569 				if (old_game.name.size()) {
570 					format_and_add_chat(
571 					   "", "", true,
572 					   (boost::format(_("The game %s has been closed")) % old_game.name).str());
573 				}
574 			}
575 
576 			gameupdate_ = true;
577 		}
578 
579 		else if (cmd == IGPCMD_CLIENTS_UPDATE) {
580 			// Client received a note, that the list of clients was changed
581 			log("InternetGaming: Client update on metaserver.\n");
582 			clientupdateonmetaserver_ = true;
583 		}
584 
585 		else if (cmd == IGPCMD_CLIENTS) {
586 			// Client received the new list of clients
587 			uint8_t number = boost::lexical_cast<int>(packet.string()) & 0xff;
588 			std::vector<InternetClient> old = clientlist_;
589 			// Push admins/registred/IRC users to a temporary list and add them back later
590 			clientlist_.clear();
591 			log("InternetGaming: Received a client list update with %u items.\n", number);
592 			InternetClient inc;
593 			for (uint8_t i = 0; i < number; ++i) {
594 				inc.name = packet.string();
595 				inc.build_id = packet.string();
596 				inc.game = packet.string();
597 				inc.type = packet.string();
598 
599 				clientlist_.push_back(inc);
600 
601 				bool found =
602 				   old.empty();  // do not show all clients, if this instance is the actual change
603 				for (InternetClient& client : old) {
604 					if (client.name == inc.name && client.type == inc.type) {
605 						found = true;
606 						client.name = "";
607 						break;
608 					}
609 				}
610 				if (!found) {
611 					format_and_add_chat(
612 					   "", "", true, (boost::format(_("%s joined the lobby")) % inc.name).str());
613 				}
614 			}
615 
616 			std::sort(clientlist_.begin(), clientlist_.end(),
617 			          [](const InternetClient& left, const InternetClient& right) {
618 				          return (left.name < right.name);
619 				       });
620 
621 			for (InternetClient& client : old) {
622 				if (client.name.size()) {
623 					format_and_add_chat(
624 					   "", "", true, (boost::format(_("%s left the lobby")) % client.name).str());
625 				}
626 			}
627 			clientupdate_ = true;
628 		}
629 
630 		else if (cmd == IGPCMD_GAME_OPEN) {
631 			// Client received the acknowledgment, that the game was opened
632 			// We can't use an assert here since this message might arrive after the game already
633 			// started
634 			if (waitcmd_ == IGPCMD_GAME_OPEN) {
635 				waitcmd_ = "";
636 			}
637 			// Get the challenge
638 			std::string challenge = packet.string();
639 			relay_password_ = crypto::sha1(challenge + authenticator_);
640 			// Save the received IP(s), so the client can connect to the game
641 			NetAddress::parse_ip(&gameips_.first, packet.string(), kInternetRelayPort);
642 			// If the next value is true, a secondary IP follows
643 			if (packet.string() == bool2str(true)) {
644 				NetAddress::parse_ip(&gameips_.second, packet.string(), kInternetRelayPort);
645 			}
646 			log("InternetGaming: Received ips of the relay to host: %s %s.\n",
647 			    gameips_.first.ip.to_string().c_str(), gameips_.second.ip.to_string().c_str());
648 			state_ = IN_GAME;
649 		}
650 
651 		else if (cmd == IGPCMD_GAME_CONNECT) {
652 			// Client received the ip for the game it wants to join
653 			assert(waitcmd_ == IGPCMD_GAME_CONNECT);
654 			waitcmd_ = "";
655 			// Save the received IP(s), so the client can connect to the game
656 			NetAddress::parse_ip(&gameips_.first, packet.string(), kInternetRelayPort);
657 			// If the next value is true, a secondary IP follows
658 			if (packet.string() == bool2str(true)) {
659 				NetAddress::parse_ip(&gameips_.second, packet.string(), kInternetRelayPort);
660 			}
661 			log("InternetGaming: Received ips of the game to join: %s %s.\n",
662 			    gameips_.first.ip.to_string().c_str(), gameips_.second.ip.to_string().c_str());
663 		}
664 
665 		else if (cmd == IGPCMD_GAME_START) {
666 			// Client received the acknowledgment, that the game was started
667 			assert(waitcmd_ == IGPCMD_GAME_START);
668 			waitcmd_ = "";
669 		}
670 
671 		else if (cmd == IGPCMD_ERROR) {
672 			// Client received an ERROR message - seems something went wrong
673 			std::string subcmd(packet.string());
674 			std::string reason(packet.string());
675 			std::string message;
676 
677 			if (subcmd == IGPCMD_CHAT) {
678 				// Something went wrong with the chat message the user sent.
679 				message += _("Chat message could not be sent.");
680 				if (reason == "NO_SUCH_USER") {
681 					message =
682 					   (boost::format("%s %s") % message % InternetGamingMessages::get_message(reason))
683 					      .str();
684 				}
685 			}
686 
687 			else if (subcmd == IGPCMD_CMD) {
688 				// Something went wrong with the command
689 				message += _("Command could not be executed.");
690 				message =
691 				   (boost::format("%s %s") % message % InternetGamingMessages::get_message(reason))
692 				      .str();
693 			}
694 
695 			else if (subcmd == IGPCMD_GAME_OPEN) {
696 				// Something went wrong with the newly opened game
697 				message = InternetGamingMessages::get_message(reason);
698 				// we got our answer, so no need to wait anymore
699 				waitcmd_ = "";
700 			}
701 
702 			else if (subcmd == IGPCMD_GAME_CONNECT && reason == "NO_SUCH_GAME") {
703 				log("InternetGaming: The game no longer exists, maybe it has just been closed\n");
704 				message = InternetGamingMessages::get_message(reason);
705 				assert(waitcmd_ == IGPCMD_GAME_CONNECT);
706 				waitcmd_ = "";
707 			}
708 			if (!message.empty()) {
709 				message = (boost::format(_("ERROR: %s")) % message).str();
710 			} else {
711 				message = (boost::format(_(
712 				              "An unexpected error message has been received about command %1%: %2%")) %
713 				           subcmd % reason)
714 				             .str();
715 			}
716 
717 			// Finally send the error message as system chat to the client.
718 			format_and_add_chat("", "", true, message);
719 		}
720 
721 		else {
722 			// Inform the client about the unknown command
723 			format_and_add_chat(
724 			   "", "", true,
725 			   (boost::format(_("Received an unknown command from the metaserver: %s")) % cmd).str());
726 		}
727 
728 	} catch (WLWarning& e) {
729 		format_and_add_chat("", "", true, e.what());
730 	}
731 }
732 
733 /// \returns Up to two NetAdress with ips of the game the client is on or wants to join
734 ///          (or the client is hosting) or invalid addresses, if no ip available.
ips()735 const std::pair<NetAddress, NetAddress>& InternetGaming::ips() {
736 	return gameips_;
737 }
738 
wait_for_ips()739 bool InternetGaming::wait_for_ips() {
740 	// Wait until the metaserver provided us with an IP address
741 	uint32_t const secs = time(nullptr);
742 	const bool is_waiting_for_connect = (waitcmd_ == IGPCMD_GAME_CONNECT);
743 	while (!gameips_.first.is_valid()) {
744 		if (error()) {
745 			return false;
746 		}
747 		if (is_waiting_for_connect && waitcmd_.empty()) {
748 			// Was trying to join a game but failed.
749 			// It probably means that the game is no longer available
750 			return false;
751 		}
752 		handle_metaserver_communication();
753 		// give some time for the answer + for a relogin, if a problem occurs.
754 		if ((kInternetGamingTimeout * 5 / 3) < time(nullptr) - secs) {
755 			return false;
756 		}
757 	}
758 	return true;
759 }
760 
relay_password()761 const std::string InternetGaming::relay_password() {
762 	return relay_password_;
763 }
764 
765 /// called by a client to join the game \arg gamename
join_game(const std::string & gamename)766 void InternetGaming::join_game(const std::string& gamename) {
767 	if (!logged_in()) {
768 		return;
769 	}
770 
771 	// Reset the game ips, we should receive new ones shortly
772 	gameips_ = std::make_pair(NetAddress(), NetAddress());
773 
774 	SendPacket s;
775 	s.string(IGPCMD_GAME_CONNECT);
776 	s.string(gamename);
777 	net->send(s);
778 	gamename_ = gamename;
779 	log("InternetGaming: Client tries to join a game with the name %s\n", gamename_.c_str());
780 	state_ = IN_GAME;
781 
782 	// From now on we wait for a reply from the metaserver
783 	waitcmd_ = IGPCMD_GAME_CONNECT;
784 	waittimeout_ = time(nullptr) + kInternetGamingTimeout;
785 }
786 
787 /// called by a client to open a new game with name gamename_
open_game()788 void InternetGaming::open_game() {
789 	if (!logged_in()) {
790 		return;
791 	}
792 
793 	// Reset the game ips, we should receive new ones shortly
794 	gameips_ = std::make_pair(NetAddress(), NetAddress());
795 
796 	SendPacket s;
797 	s.string(IGPCMD_GAME_OPEN);
798 	s.string(gamename_);
799 	net->send(s);
800 	log("InternetGaming: Client opened a game with the name %s.\n", gamename_.c_str());
801 
802 	// From now on we wait for a reply from the metaserver
803 	waitcmd_ = IGPCMD_GAME_OPEN;
804 	waittimeout_ = time(nullptr) + kInternetGamingTimeout;
805 }
806 
807 /// called by a client that is host of a game to inform the metaserver, that the game started
set_game_playing()808 void InternetGaming::set_game_playing() {
809 	if (!logged_in()) {
810 		return;
811 	}
812 
813 	SendPacket s;
814 	s.string(IGPCMD_GAME_START);
815 	net->send(s);
816 	log("InternetGaming: Client announced the start of the game %s.\n", gamename_.c_str());
817 
818 	// From now on we wait for a reply from the metaserver
819 	waitcmd_ = IGPCMD_GAME_START;
820 	waittimeout_ = time(nullptr) + kInternetGamingTimeout;
821 }
822 
823 /// called by a client to inform the metaserver, that it left the game and is back in the lobby.
824 /// If this is called by the hosting client, this further informs the metaserver, that the game was
825 /// closed.
set_game_done()826 void InternetGaming::set_game_done() {
827 	if (!logged_in()) {
828 		return;
829 	}
830 
831 	SendPacket s;
832 	s.string(IGPCMD_GAME_DISCONNECT);
833 	net->send(s);
834 
835 	gameips_ = std::make_pair(NetAddress(), NetAddress());
836 	state_ = LOBBY;
837 
838 	log("InternetGaming: Client announced the disconnect from the game %s.\n", gamename_.c_str());
839 }
840 
841 /// \returns whether the local gamelist was updated
842 /// \note this function resets gameupdate_. So if you call it, please really handle the output.
update_for_games()843 bool InternetGaming::update_for_games() {
844 	bool temp = gameupdate_;
845 	gameupdate_ = false;
846 	return temp;
847 }
848 
849 /// \returns the tables in the room, if no error occured, or nullptr in case of error
games()850 const std::vector<InternetGame>* InternetGaming::games() {
851 	return error() ? nullptr : &gamelist_;
852 }
853 
854 /// \returns whether the local clientlist_ was updated
855 /// \note this function resets clientupdate_. So if you call it, please really handle the output.
update_for_clients()856 bool InternetGaming::update_for_clients() {
857 	bool temp = clientupdate_;
858 	clientupdate_ = false;
859 	return temp;
860 }
861 
862 /// \returns the players in the room, if no error occured, or nullptr in case of error
clients()863 const std::vector<InternetClient>* InternetGaming::clients() {
864 	return error() ? nullptr : &clientlist_;
865 }
866 
867 /// ChatProvider: sends a message via the metaserver.
send(const std::string & msg)868 void InternetGaming::send(const std::string& msg) {
869 	// TODO(Notabilis): Messages can get lost when we are temporarily disconnected from the
870 	// metaserver,
871 	// even when we reconnect again. "Answered" messages like IGPCMD_GAME_CONNECT are resent but chat
872 	// messages are not. Resend them after some time when we did not receive the matching IGPCMD_CHAT
873 	// command from the server? For global/public messages we could wait for the returned IGPCMD_CHAT
874 	// from the metaserver, similar to other commands. What about private messages? Maybe modify the
875 	// metaserver to send them back, too?
876 	if (!logged_in()) {
877 		format_and_add_chat(
878 		   "", "", true, _("Message could not be sent: You are not connected to the metaserver!"));
879 		return;
880 	}
881 
882 	std::string trimmed = boost::algorithm::trim_copy(msg);
883 	if (trimmed.empty()) {
884 		// Message is empty or only space characters. We don't want it either way
885 		return;
886 	}
887 
888 	SendPacket s;
889 	s.string(IGPCMD_CHAT);
890 
891 	if (*msg.begin() == '@') {
892 		// Format a personal message
893 		std::string::size_type const space = msg.find(' ');
894 		if (space >= msg.size() - 1) {
895 			format_and_add_chat(
896 			   "", "", true,
897 			   _("Message could not be sent: Was this supposed to be a private message?"));
898 			return;
899 		}
900 		trimmed = boost::algorithm::trim_copy(msg.substr(space + 1));
901 		if (trimmed.empty()) {
902 			format_and_add_chat(
903 			   "", "", true,
904 			   _("Message could not be sent: Was this supposed to be a private message?"));
905 			return;
906 		}
907 
908 		s.string(trimmed);                   // message
909 		s.string(msg.substr(1, space - 1));  // recipient
910 
911 		format_and_add_chat(clientname_, msg.substr(1, space - 1), false, msg.substr(space + 1));
912 
913 	} else if (clientrights_ == INTERNET_CLIENT_SUPERUSER && *msg.begin() == '/') {
914 		// This is either a /me command, a super user command, or well... just a chat message
915 		// beginning
916 		// with a "/" - let's see...
917 
918 		if (msg == "/help") {
919 			format_and_add_chat("", "", true, _("Supported admin commands:"));
920 			format_and_add_chat("", "", true, _("/motd <msg> - Set a permanent greeting message"));
921 			format_and_add_chat("", "", true, _("/announce <msg> - Send a one time system message"));
922 			format_and_add_chat(
923 			   "", "", true,
924 			   _("/warn <user> <msg> - Send a private system message to the given user"));
925 			format_and_add_chat(
926 			   "", "", true,
927 			   _("/kick <user|game> - Remove the given user or game from the metaserver"));
928 			format_and_add_chat(
929 			   "", "", true, _("/ban <user> - Ban a user for 24 hours from the metaserver"));
930 			return;
931 		}
932 
933 		// Split up in "cmd" "arg"
934 		std::string cmd, arg;
935 		std::string temp = msg.substr(1);  // cut off '/'
936 		std::string::size_type const space = temp.find(' ');
937 		if (space > temp.size()) {
938 			// no argument
939 			goto normal;
940 		}
941 
942 		// get the cmd and the arg
943 		cmd = temp.substr(0, space);
944 		arg = boost::algorithm::trim_copy(temp.substr(space + 1));
945 
946 		if (!arg.empty() && cmd == "motd") {
947 			SendPacket m;
948 			m.string(IGPCMD_MOTD);
949 			// Check whether motd is attached or should be loaded from a file
950 			if (arg.size() > 1 && arg.at(0) == '%') {
951 				// Seems we should load the motd from a file
952 				temp = arg.substr(1);  // cut of the "%"
953 				if (g_fs->file_exists(temp) && !g_fs->is_directory(temp)) {
954 					// Read in the file
955 					FileRead fr;
956 					fr.open(*g_fs, temp);
957 					if (!fr.end_of_file()) {
958 						arg = fr.read_line();
959 						while (!fr.end_of_file()) {
960 							arg += fr.read_line();
961 						}
962 					}
963 				}
964 			}
965 			// send the request to change the motd
966 			m.string(arg);
967 			net->send(m);
968 			return;
969 		} else if (!arg.empty() && cmd == "announce") {
970 			// send the request to make an announcement
971 			SendPacket m;
972 			m.string(IGPCMD_ANNOUNCEMENT);
973 			m.string(arg);
974 			net->send(m);
975 			return;
976 		} else if (!arg.empty() && (cmd == "warn" || cmd == "kick" || cmd == "ban")) {
977 			// warn a user by sending a private system message or
978 			// kick a user (for 5 minutes) or a game from the metaserver or
979 			// ban a user for 24 hours
980 			SendPacket m;
981 			m.string(IGPCMD_CMD);
982 			m.string(cmd);
983 			m.string(arg);
984 			net->send(m);
985 			return;
986 		} else {
987 			// let everything else pass
988 			goto normal;
989 		}
990 	} else {
991 	normal:
992 		s.string(msg);
993 		s.string("");
994 	}
995 
996 	net->send(s);
997 }
998 
999 /**
1000  * \returns the boolean value of a string received from the metaserver.
1001  * If conversion fails, it throws a \ref warning
1002  */
str2bool(std::string str)1003 bool InternetGaming::str2bool(std::string str) {
1004 	if ((str != "true") && (str != "false")) {
1005 		throw WLWarning(_("Conversion error"),
1006 		                /** TRANSLATORS: Geeky message from the metaserver */
1007 		                /** TRANSLATORS: This message is shown if %s isn't "true" or "false" */
1008 		                _("Unable to determine truth value for \"%s\""), str.c_str());
1009 	}
1010 	return str == "true";
1011 }
1012 
1013 /// \returns a string containing the boolean value \arg b to be send to metaserver
bool2str(bool b)1014 std::string InternetGaming::bool2str(bool b) {
1015 	return b ? "true" : "false";
1016 }
1017 
1018 /// formates a chat message and adds it to the list of chat messages
format_and_add_chat(const std::string & from,const std::string & to,bool system,const std::string & msg)1019 void InternetGaming::format_and_add_chat(const std::string& from,
1020                                          const std::string& to,
1021                                          bool system,
1022                                          const std::string& msg) {
1023 	ChatMessage c(msg);
1024 	if (!system && from.empty()) {
1025 		std::string unkown_string =
1026 		   (boost::format("<%s>") % pgettext("chat_sender", "Unknown")).str();
1027 		c.sender = unkown_string;
1028 	} else {
1029 		c.sender = from;
1030 	}
1031 	c.playern = system ? -1 : to.size() ? 3 : 7;
1032 	c.recipient = to;
1033 
1034 	receive(c);
1035 	if (system && (state_ == IN_GAME)) {
1036 		// Save system chat messages separately as well, so the nethost can import and show them in
1037 		// game;
1038 		c.msg = "METASERVER: " + msg;
1039 		ingame_system_chat_.push_back(c);
1040 	}
1041 }
1042 
1043 /**
1044  * Check for vaild username characters.
1045  */
valid_username(std::string username)1046 bool InternetGaming::valid_username(std::string username) {
1047 	if (username.empty() ||
1048 	    username.find_first_not_of("abcdefghijklmnopqrstuvwxyz"
1049 	                               "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890@.+-_") <= username.size()) {
1050 		return false;
1051 	}
1052 	return true;
1053 }
1054