/* * This file is part of OpenTTD. * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . */ /** @file network_coordinator.cpp Game Coordinator sending/receiving part of the network protocol. */ #include "../stdafx.h" #include "../debug.h" #include "../error.h" #include "../rev.h" #include "../settings_type.h" #include "../strings_func.h" #include "../window_func.h" #include "../window_type.h" #include "network.h" #include "network_coordinator.h" #include "network_gamelist.h" #include "network_internal.h" #include "table/strings.h" #include "../safeguards.h" static const auto NETWORK_COORDINATOR_DELAY_BETWEEN_UPDATES = std::chrono::seconds(30); ///< How many time between updates the server sends to the Game Coordinator. ClientNetworkCoordinatorSocketHandler _network_coordinator_client; ///< The connection to the Game Coordinator. ConnectionType _network_server_connection_type = CONNECTION_TYPE_UNKNOWN; ///< What type of connection the Game Coordinator detected we are on. std::string _network_server_invite_code = ""; ///< Our invite code as indicated by the Game Coordinator. /** Connect to a game server by IP:port. */ class NetworkDirectConnecter : public TCPConnecter { private: std::string token; ///< Token of this connection. uint8 tracking_number; ///< Tracking number of this connection. public: /** * Try to establish a direct (hostname:port based) connection. * @param hostname The hostname of the server. * @param port The port of the server. * @param token The token as given by the Game Coordinator to track this connection attempt. * @param tracking_number The tracking number as given by the Game Coordinator to track this connection attempt. */ NetworkDirectConnecter(const std::string &hostname, uint16 port, const std::string &token, uint8 tracking_number) : TCPConnecter(hostname, port), token(token), tracking_number(tracking_number) {} void OnFailure() override { _network_coordinator_client.ConnectFailure(this->token, this->tracking_number); } void OnConnect(SOCKET s) override { _network_coordinator_client.ConnectSuccess(this->token, s); } }; /** Connect to the Game Coordinator server. */ class NetworkCoordinatorConnecter : TCPConnecter { public: /** * Initiate the connecting. * @param connection_string The address of the Game Coordinator server. */ NetworkCoordinatorConnecter(const std::string &connection_string) : TCPConnecter(connection_string, NETWORK_COORDINATOR_SERVER_PORT) {} void OnFailure() override { _network_coordinator_client.connecting = false; _network_coordinator_client.CloseConnection(true); } void OnConnect(SOCKET s) override { assert(_network_coordinator_client.sock == INVALID_SOCKET); _network_coordinator_client.sock = s; _network_coordinator_client.last_activity = std::chrono::steady_clock::now(); _network_coordinator_client.connecting = false; } }; bool ClientNetworkCoordinatorSocketHandler::Receive_GC_ERROR(Packet *p) { NetworkCoordinatorErrorType error = (NetworkCoordinatorErrorType)p->Recv_uint8(); std::string detail = p->Recv_string(NETWORK_ERROR_DETAIL_LENGTH); switch (error) { case NETWORK_COORDINATOR_ERROR_UNKNOWN: this->CloseConnection(); return false; case NETWORK_COORDINATOR_ERROR_REGISTRATION_FAILED: SetDParamStr(0, detail); ShowErrorMessage(STR_NETWORK_ERROR_COORDINATOR_REGISTRATION_FAILED, STR_JUST_RAW_STRING, WL_ERROR); /* To prevent that we constantly try to reconnect, switch to private game. */ _settings_client.network.server_advertise = false; this->CloseConnection(); return false; case NETWORK_COORDINATOR_ERROR_INVALID_INVITE_CODE: { /* Find the connecter based on the invite code. */ auto connecter_it = this->connecter_pre.find(detail); if (connecter_it == this->connecter_pre.end()) return true; this->connecter_pre.erase(connecter_it); /* Mark the server as offline. */ NetworkGameList *item = NetworkGameListAddItem(detail); item->online = false; UpdateNetworkGameWindow(); return true; } default: Debug(net, 0, "Invalid error type {} received from Game Coordinator", error); this->CloseConnection(); return false; } } bool ClientNetworkCoordinatorSocketHandler::Receive_GC_REGISTER_ACK(Packet *p) { /* Schedule sending an update. */ this->next_update = std::chrono::steady_clock::now(); _settings_client.network.server_invite_code = p->Recv_string(NETWORK_INVITE_CODE_LENGTH); _settings_client.network.server_invite_code_secret = p->Recv_string(NETWORK_INVITE_CODE_SECRET_LENGTH); _network_server_connection_type = (ConnectionType)p->Recv_uint8(); if (_network_server_connection_type == CONNECTION_TYPE_ISOLATED) { ShowErrorMessage(STR_NETWORK_ERROR_COORDINATOR_ISOLATED, STR_NETWORK_ERROR_COORDINATOR_ISOLATED_DETAIL, WL_ERROR); } /* Users can change the invite code in the settings, but this has no effect * on the invite code as assigned by the server. So * _network_server_invite_code contains the current invite code, * and _settings_client.network.server_invite_code contains the one we will * attempt to re-use when registering again. */ _network_server_invite_code = _settings_client.network.server_invite_code; SetWindowDirty(WC_CLIENT_LIST, 0); if (_network_dedicated) { std::string connection_type; switch (_network_server_connection_type) { case CONNECTION_TYPE_ISOLATED: connection_type = "Remote players can't connect"; break; case CONNECTION_TYPE_DIRECT: connection_type = "Public"; break; case CONNECTION_TYPE_UNKNOWN: // Never returned from Game Coordinator. default: connection_type = "Unknown"; break; // Should never happen, but don't fail if it does. } Debug(net, 3, "----------------------------------------"); Debug(net, 3, "Your server is now registered with the Game Coordinator:"); Debug(net, 3, " Game type: Public"); Debug(net, 3, " Connection type: {}", connection_type); Debug(net, 3, " Invite code: {}", _network_server_invite_code); Debug(net, 3, "----------------------------------------"); } else { Debug(net, 3, "Game Coordinator registered our server with invite code '{}'", _network_server_invite_code); } return true; } bool ClientNetworkCoordinatorSocketHandler::Receive_GC_LISTING(Packet *p) { uint8 servers = p->Recv_uint16(); /* End of list; we can now remove all expired items from the list. */ if (servers == 0) { NetworkGameListRemoveExpired(); return true; } for (; servers > 0; servers--) { std::string connection_string = p->Recv_string(NETWORK_HOSTNAME_PORT_LENGTH); /* Read the NetworkGameInfo from the packet. */ NetworkGameInfo ngi = {}; DeserializeNetworkGameInfo(p, &ngi); /* Now we know the connection string, we can add it to our list. */ NetworkGameList *item = NetworkGameListAddItem(connection_string); /* Clear any existing GRFConfig chain. */ ClearGRFConfigList(&item->info.grfconfig); /* Copy the new NetworkGameInfo info. */ item->info = ngi; /* Check for compatability with the client. */ CheckGameCompatibility(item->info); /* Mark server as online. */ item->online = true; /* Mark the item as up-to-date. */ item->version = _network_game_list_version; } UpdateNetworkGameWindow(); return true; } bool ClientNetworkCoordinatorSocketHandler::Receive_GC_CONNECTING(Packet *p) { std::string token = p->Recv_string(NETWORK_TOKEN_LENGTH); std::string invite_code = p->Recv_string(NETWORK_INVITE_CODE_LENGTH); /* Find the connecter based on the invite code. */ auto connecter_it = this->connecter_pre.find(invite_code); if (connecter_it == this->connecter_pre.end()) { this->CloseConnection(); return false; } /* Now store it based on the token. */ this->connecter[token] = connecter_it->second; this->connecter_pre.erase(connecter_it); return true; } bool ClientNetworkCoordinatorSocketHandler::Receive_GC_CONNECT_FAILED(Packet *p) { std::string token = p->Recv_string(NETWORK_TOKEN_LENGTH); auto connecter_it = this->connecter.find(token); if (connecter_it != this->connecter.end()) { connecter_it->second->SetFailure(); this->connecter.erase(connecter_it); } /* Close all remaining connections. */ this->CloseToken(token); return true; } bool ClientNetworkCoordinatorSocketHandler::Receive_GC_DIRECT_CONNECT(Packet *p) { std::string token = p->Recv_string(NETWORK_TOKEN_LENGTH); uint8 tracking_number = p->Recv_uint8(); std::string hostname = p->Recv_string(NETWORK_HOSTNAME_LENGTH); uint16 port = p->Recv_uint16(); /* Ensure all other pending connection attempts are killed. */ if (this->game_connecter != nullptr) { this->game_connecter->Kill(); this->game_connecter = nullptr; } this->game_connecter = new NetworkDirectConnecter(hostname, port, token, tracking_number); return true; } void ClientNetworkCoordinatorSocketHandler::Connect() { /* We are either already connected or are trying to connect. */ if (this->sock != INVALID_SOCKET || this->connecting) return; this->Reopen(); this->connecting = true; this->last_activity = std::chrono::steady_clock::now(); new NetworkCoordinatorConnecter(NetworkCoordinatorConnectionString()); } NetworkRecvStatus ClientNetworkCoordinatorSocketHandler::CloseConnection(bool error) { NetworkCoordinatorSocketHandler::CloseConnection(error); this->CloseSocket(); this->connecting = false; _network_server_connection_type = CONNECTION_TYPE_UNKNOWN; this->next_update = {}; this->CloseAllTokens(); SetWindowDirty(WC_CLIENT_LIST, 0); return NETWORK_RECV_STATUS_OKAY; } /** * Register our server to receive our invite code. */ void ClientNetworkCoordinatorSocketHandler::Register() { _network_server_connection_type = CONNECTION_TYPE_UNKNOWN; this->next_update = {}; SetWindowDirty(WC_CLIENT_LIST, 0); this->Connect(); Packet *p = new Packet(PACKET_COORDINATOR_SERVER_REGISTER); p->Send_uint8(NETWORK_COORDINATOR_VERSION); p->Send_uint8(SERVER_GAME_TYPE_PUBLIC); p->Send_uint16(_settings_client.network.server_port); if (_settings_client.network.server_invite_code.empty() || _settings_client.network.server_invite_code_secret.empty()) { p->Send_string(""); p->Send_string(""); } else { p->Send_string(_settings_client.network.server_invite_code); p->Send_string(_settings_client.network.server_invite_code_secret); } this->SendPacket(p); } /** * Send an update of our server status to the Game Coordinator. */ void ClientNetworkCoordinatorSocketHandler::SendServerUpdate() { Debug(net, 6, "Sending server update to Game Coordinator"); this->next_update = std::chrono::steady_clock::now() + NETWORK_COORDINATOR_DELAY_BETWEEN_UPDATES; Packet *p = new Packet(PACKET_COORDINATOR_SERVER_UPDATE, TCP_MTU); p->Send_uint8(NETWORK_COORDINATOR_VERSION); SerializeNetworkGameInfo(p, GetCurrentNetworkServerGameInfo()); this->SendPacket(p); } /** * Request a listing of all public servers. */ void ClientNetworkCoordinatorSocketHandler::GetListing() { this->Connect(); _network_game_list_version++; Packet *p = new Packet(PACKET_COORDINATOR_CLIENT_LISTING); p->Send_uint8(NETWORK_COORDINATOR_VERSION); p->Send_uint8(NETWORK_GAME_INFO_VERSION); p->Send_string(_openttd_revision); this->SendPacket(p); } /** * Join a server based on an invite code. * @param invite_code The invite code of the server to connect to. * @param connecter The connecter of the request. */ void ClientNetworkCoordinatorSocketHandler::ConnectToServer(const std::string &invite_code, TCPServerConnecter *connecter) { assert(StrStartsWith(invite_code, "+")); if (this->connecter_pre.find(invite_code) != this->connecter_pre.end()) { /* If someone is hammering the refresh key, one can sent out two * requests for the same invite code. There isn't really a great way * of handling this, so just ignore this request. */ connecter->SetFailure(); return; } /* Initially we store based on invite code; on first reply we know the * token, and will start using that key instead. */ this->connecter_pre[invite_code] = connecter; this->Connect(); Packet *p = new Packet(PACKET_COORDINATOR_CLIENT_CONNECT); p->Send_uint8(NETWORK_COORDINATOR_VERSION); p->Send_string(invite_code); this->SendPacket(p); } /** * Callback from a Connecter to let the Game Coordinator know the connection failed. * @param token Token of the connecter that failed. * @param tracking_number Tracking number of the connecter that failed. */ void ClientNetworkCoordinatorSocketHandler::ConnectFailure(const std::string &token, uint8 tracking_number) { /* Connecter will destroy itself. */ this->game_connecter = nullptr; Packet *p = new Packet(PACKET_COORDINATOR_SERCLI_CONNECT_FAILED); p->Send_uint8(NETWORK_COORDINATOR_VERSION); p->Send_string(token); p->Send_uint8(tracking_number); this->SendPacket(p); auto connecter_it = this->connecter.find(token); assert(connecter_it != this->connecter.end()); connecter_it->second->SetFailure(); this->connecter.erase(connecter_it); } /** * Callback from a Connecter to let the Game Coordinator know the connection * to the game server is established. * @param token Token of the connecter that succeeded. * @param sock The socket that the connecter can now use. */ void ClientNetworkCoordinatorSocketHandler::ConnectSuccess(const std::string &token, SOCKET sock) { /* Connecter will destroy itself. */ this->game_connecter = nullptr; assert(!_network_server); Packet *p = new Packet(PACKET_COORDINATOR_CLIENT_CONNECTED); p->Send_uint8(NETWORK_COORDINATOR_VERSION); p->Send_string(token); this->SendPacket(p); auto connecter_it = this->connecter.find(token); assert(connecter_it != this->connecter.end()); connecter_it->second->SetConnected(sock); this->connecter.erase(connecter_it); /* Close all remaining connections. */ this->CloseToken(token); } /** * Close everything related to this connection token. * @param token The connection token to close. */ void ClientNetworkCoordinatorSocketHandler::CloseToken(const std::string &token) { /* Ensure all other pending connection attempts are also killed. */ if (this->game_connecter != nullptr) { this->game_connecter->Kill(); this->game_connecter = nullptr; } } /** * Close all pending connection tokens. */ void ClientNetworkCoordinatorSocketHandler::CloseAllTokens() { /* Ensure all other pending connection attempts are also killed. */ if (this->game_connecter != nullptr) { this->game_connecter->Kill(); this->game_connecter = nullptr; } /* Mark any pending connecters as failed. */ for (auto &[token, it] : this->connecter) { it->SetFailure(); } for (auto &[invite_code, it] : this->connecter_pre) { it->SetFailure(); } this->connecter.clear(); this->connecter_pre.clear(); } /** * Check whether we received/can send some data from/to the Game Coordinator server and * when that's the case handle it appropriately. */ void ClientNetworkCoordinatorSocketHandler::SendReceive() { /* Private games are not listed via the Game Coordinator. */ if (_network_server && !_settings_client.network.server_advertise) { if (this->sock != INVALID_SOCKET) { this->CloseConnection(); } return; } static int last_attempt_backoff = 1; static bool first_reconnect = true; if (this->sock == INVALID_SOCKET) { static std::chrono::steady_clock::time_point last_attempt = {}; /* Don't auto-reconnect when we are not a server. */ if (!_network_server) return; /* Don't reconnect if we are connecting. */ if (this->connecting) return; /* Throttle how often we try to reconnect. */ if (std::chrono::steady_clock::now() < last_attempt + std::chrono::seconds(1) * last_attempt_backoff) return; last_attempt = std::chrono::steady_clock::now(); /* Delay reconnecting with up to 32 seconds. */ if (last_attempt_backoff < 32) { last_attempt_backoff *= 2; } /* Do not reconnect on the first attempt, but only initialize the * last_attempt variables. Otherwise after an outage all servers * reconnect at the same time, potentially overwhelming the * Game Coordinator. */ if (first_reconnect) { first_reconnect = false; return; } Debug(net, 1, "Connection with Game Coordinator lost; reconnecting..."); this->Register(); return; } last_attempt_backoff = 1; first_reconnect = true; if (_network_server && _network_server_connection_type != CONNECTION_TYPE_UNKNOWN && std::chrono::steady_clock::now() > this->next_update) { this->SendServerUpdate(); } if (!_network_server && std::chrono::steady_clock::now() > this->last_activity + IDLE_TIMEOUT) { this->CloseConnection(); return; } if (this->CanSendReceive()) { if (this->ReceivePackets()) { this->last_activity = std::chrono::steady_clock::now(); } } this->SendPackets(); }