#pragma region Copyright (c) 2014-2018 OpenRCT2 Developers /***************************************************************************** * OpenRCT2, an open source clone of Roller Coaster Tycoon 2. * * OpenRCT2 is the work of many authors, a full list can be found in contributors.md * For more information, visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 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, either version 3 of the License, or * (at your option) any later version. * * A full copy of the GNU General Public License can be found in licence.txt *****************************************************************************/ #pragma endregion #ifndef DISABLE_NETWORK #include #include "../core/Console.hpp" #include "../core/String.hpp" #include "../core/Util.hpp" #include "network.h" #include "NetworkServerAdvertiser.h" #include "../config/Config.h" #include "../localisation/Date.h" #include "../management/Finance.h" #include "../peep/Peep.h" #include "../platform/platform.h" #include "../util/Util.h" #include "../world/Map.h" #include "../world/Park.h" #include "Http.h" using namespace OpenRCT2::Network; #ifndef DISABLE_HTTP enum MASTER_SERVER_STATUS { MASTER_SERVER_STATUS_OK = 200, MASTER_SERVER_STATUS_INVALID_TOKEN = 401, MASTER_SERVER_STATUS_SERVER_NOT_FOUND = 404, MASTER_SERVER_STATUS_INTERNAL_ERROR = 500 }; constexpr sint32 MASTER_SERVER_REGISTER_TIME = 120 * 1000; // 2 minutes constexpr sint32 MASTER_SERVER_HEARTBEAT_TIME = 60 * 1000; // 1 minute class NetworkServerAdvertiser final : public INetworkServerAdvertiser { private: uint16 _port; ADVERTISE_STATUS _status = ADVERTISE_STATUS::UNREGISTERED; uint32 _lastAdvertiseTime = 0; uint32 _lastHeartbeatTime = 0; // Our unique token for this server std::string _token; // Key received from the master server std::string _key; // See https://github.com/OpenRCT2/OpenRCT2/issues/6277 and 4953 bool _forceIPv4 = false; public: explicit NetworkServerAdvertiser(uint16 port) { _port = port; _key = GenerateAdvertiseKey(); } ADVERTISE_STATUS GetStatus() const override { return _status; } void Update() override { switch (_status) { case ADVERTISE_STATUS::UNREGISTERED: if (_lastAdvertiseTime == 0 || platform_get_ticks() > _lastAdvertiseTime + MASTER_SERVER_REGISTER_TIME) { SendRegistration(_forceIPv4); } break; case ADVERTISE_STATUS::REGISTERED: if (platform_get_ticks() > _lastHeartbeatTime + MASTER_SERVER_HEARTBEAT_TIME) { SendHeartbeat(); } break; // exhaust enum values to satisfy clang case ADVERTISE_STATUS::DISABLED: break; } } private: void SendRegistration(bool forceIPv4) { _lastAdvertiseTime = platform_get_ticks(); // Send the registration request Http::Request request; request.url = GetMasterServerUrl(); request.method = Http::Method::POST; request.forceIPv4 = forceIPv4; json_t *body = json_object(); json_object_set_new(body, "key", json_string(_key.c_str())); json_object_set_new(body, "port", json_integer(_port)); request.body = json_dumps(body, JSON_COMPACT); request.header["Content-Type"] = "application/json"; Http::DoAsync(request, [&](Http::Response response) -> void { if (response.status != Http::Status::OK) { Console::WriteLine("Unable to connect to master server"); return; } json_t * root = Json::FromString(response.body); this->OnRegistrationResponse(root); }); json_decref(body); } void SendHeartbeat() { Http::Request request; request.url = GetMasterServerUrl(); request.method = Http::Method::POST; json_t * body = GetHeartbeatJson(); request.body = json_dumps(body, JSON_COMPACT); request.header["Content-Type"] = "application/json"; _lastHeartbeatTime = platform_get_ticks(); Http::DoAsync(request, [&](Http::Response response) -> void { if (response.status != Http::Status::OK) { Console::WriteLine("Unable to connect to master server"); return; } json_t * root = Json::FromString(response.body); this->OnHeartbeatResponse(root); }); json_decref(body); } void OnRegistrationResponse(json_t * jsonRoot) { json_t *jsonStatus = json_object_get(jsonRoot, "status"); if (json_is_integer(jsonStatus)) { sint32 status = (sint32)json_integer_value(jsonStatus); if (status == MASTER_SERVER_STATUS_OK) { json_t * jsonToken = json_object_get(jsonRoot, "token"); if (json_is_string(jsonToken)) { _token = std::string(json_string_value(jsonToken)); _status = ADVERTISE_STATUS::REGISTERED; } } else { const char * message = "Invalid response from server"; json_t * jsonMessage = json_object_get(jsonRoot, "message"); if (json_is_string(jsonMessage)) { message = json_string_value(jsonMessage); } Console::Error::WriteLine("Unable to advertise (%d): %s", status, message); // Hack for https://github.com/OpenRCT2/OpenRCT2/issues/6277 // Master server may not reply correctly if using IPv6, retry forcing IPv4, // don't wait the full timeout. if (!_forceIPv4 && status == 500) { _forceIPv4 = true; _lastAdvertiseTime = 0; log_info("Retry with ipv4 only"); } } } } void OnHeartbeatResponse(json_t * jsonRoot) { json_t *jsonStatus = json_object_get(jsonRoot, "status"); if (json_is_integer(jsonStatus)) { sint32 status = (sint32)json_integer_value(jsonStatus); if (status == MASTER_SERVER_STATUS_OK) { // Master server has successfully updated our server status } else if (status == MASTER_SERVER_STATUS_INVALID_TOKEN) { _status = ADVERTISE_STATUS::UNREGISTERED; Console::WriteLine("Master server heartbeat failed: Invalid Token"); } } } json_t * GetHeartbeatJson() { uint32 numPlayers = network_get_num_players(); json_t * root = json_object(); json_object_set_new(root, "token", json_string(_token.c_str())); json_object_set_new(root, "players", json_integer(numPlayers)); json_t * gameInfo = json_object(); json_object_set_new(gameInfo, "mapSize", json_integer(gMapSize - 2)); json_object_set_new(gameInfo, "day", json_integer(gDateMonthTicks)); json_object_set_new(gameInfo, "month", json_integer(gDateMonthsElapsed)); json_object_set_new(gameInfo, "guests", json_integer(gNumGuestsInPark)); json_object_set_new(gameInfo, "parkValue", json_integer(gParkValue)); if (!(gParkFlags & PARK_FLAGS_NO_MONEY)) { json_object_set_new(gameInfo, "cash", json_integer(gCash)); } json_object_set_new(root, "gameInfo", gameInfo); return root; } static std::string GenerateAdvertiseKey() { // Generate a string of 16 random hex characters (64-integer key as a hex formatted string) static constexpr const char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; char key[17]; for (sint32 i = 0; i < 16; i++) { sint32 hexCharIndex = util_rand() % Util::CountOf(hexChars); key[i] = hexChars[hexCharIndex]; } key[Util::CountOf(key) - 1] = 0; return key; } static const char * GetMasterServerUrl() { const char * result = OPENRCT2_MASTER_SERVER_URL; if (!String::IsNullOrEmpty(gConfigNetwork.master_server_url)) { result = gConfigNetwork.master_server_url; } return result; } }; INetworkServerAdvertiser * CreateServerAdvertiser(uint16 port) { return new NetworkServerAdvertiser(port); } #else // DISABLE_HTTP class DummyNetworkServerAdvertiser final : public INetworkServerAdvertiser { public: virtual ADVERTISE_STATUS GetStatus() const override { return ADVERTISE_STATUS::DISABLED; }; virtual void Update() override {}; }; INetworkServerAdvertiser * CreateServerAdvertiser(uint16 port) { return new DummyNetworkServerAdvertiser(); } #endif // DISABLE_HTTP #endif // DISABLE_NETWORK