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