From 827e40d78a33dae5e9eb6c4a3a6899529b825767 Mon Sep 17 00:00:00 2001 From: spycrab Date: Sat, 30 Mar 2019 14:48:46 +0100 Subject: [PATCH] UICommon: Add NetPlayIndex helper --- Source/Core/UICommon/CMakeLists.txt | 1 + Source/Core/UICommon/NetPlayIndex.cpp | 321 ++++++++++++++++++++++++++ Source/Core/UICommon/NetPlayIndex.h | 65 ++++++ Source/Core/UICommon/UICommon.vcxproj | 4 +- 4 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 Source/Core/UICommon/NetPlayIndex.cpp create mode 100644 Source/Core/UICommon/NetPlayIndex.h diff --git a/Source/Core/UICommon/CMakeLists.txt b/Source/Core/UICommon/CMakeLists.txt index 0c665a3f20..46f212490b 100644 --- a/Source/Core/UICommon/CMakeLists.txt +++ b/Source/Core/UICommon/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(uicommon DiscordPresence.cpp GameFile.cpp GameFileCache.cpp + NetPlayIndex.cpp ResourcePack/Manager.cpp ResourcePack/Manifest.cpp ResourcePack/ResourcePack.cpp diff --git a/Source/Core/UICommon/NetPlayIndex.cpp b/Source/Core/UICommon/NetPlayIndex.cpp new file mode 100644 index 0000000000..9092a96350 --- /dev/null +++ b/Source/Core/UICommon/NetPlayIndex.cpp @@ -0,0 +1,321 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "UICommon/NetPlayIndex.h" + +#include + +#include "Common/HttpRequest.h" +#include "Common/Thread.h" +#include "Common/Version.h" + +#include "Core/Config/NetplaySettings.h" + +NetPlayIndex::NetPlayIndex() +{ +} + +NetPlayIndex::~NetPlayIndex() +{ + if (!m_secret.empty()) + Remove(); +} + +static std::optional ParseResponse(std::vector response) +{ + std::string response_string(reinterpret_cast(response.data()), response.size()); + + picojson::value json; + + auto error = picojson::parse(json, response_string); + + if (!error.empty()) + return {}; + + return json; +} + +std::optional> +NetPlayIndex::List(const std::map& filters) +{ + Common::HttpRequest request; + + std::string list_url = Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/list"; + + if (!filters.empty()) + { + list_url += "?"; + for (const auto& filter : filters) + { + list_url += filter.first + "=" + request.EscapeComponent(filter.second) + "&"; + } + list_url = list_url.substr(0, list_url.size() - 1); + } + + auto response = request.Get(list_url); + + if (!response) + { + m_last_error = "NO_RESPONSE"; + return {}; + } + + auto json = ParseResponse(response.value()); + + if (!json) + { + m_last_error = "BAD_JSON"; + return {}; + } + + const auto& status = json->get("status"); + + if (status.to_str() != "OK") + { + m_last_error = status.to_str(); + return {}; + } + + const auto& entries = json->get("sessions"); + + std::vector sessions; + + for (const auto& entry : entries.get()) + { + NetPlaySession session; + + const auto& name = entry.get("name"); + const auto& region = entry.get("region"); + const auto& method = entry.get("method"); + const auto& game_id = entry.get("game"); + const auto& server_id = entry.get("server_id"); + const auto& has_password = entry.get("password"); + const auto& player_count = entry.get("player_count"); + const auto& port = entry.get("port"); + const auto& in_game = entry.get("in_game"); + + if (!name.is() || !region.is() || !method.is() || + !server_id.is() || !game_id.is() || !has_password.is() || + !player_count.is() || !port.is() || !in_game.is()) + continue; + + session.name = name.to_str(); + session.region = region.to_str(); + session.game_id = game_id.to_str(); + session.server_id = server_id.to_str(); + session.method = method.to_str(); + session.has_password = has_password.get(); + session.player_count = static_cast(player_count.get()); + session.port = static_cast(port.get()); + session.in_game = in_game.get(); + + sessions.push_back(session); + } + + return sessions; +} + +void NetPlayIndex::NotificationLoop() +{ + while (m_running.IsSet()) + { + Common::HttpRequest request; + auto response = request.Get( + Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/session/active?secret=" + m_secret + + "&player_count=" + std::to_string(m_player_count) + + "&game=" + request.EscapeComponent(m_game) + "&in_game=" + std::to_string(m_in_game)); + + if (!response) + continue; + + auto json = ParseResponse(response.value()); + + if (!json) + { + m_last_error = "BAD_JSON"; + m_running.Set(false); + return; + } + + std::string status = json->get("status").to_str(); + + if (status != "OK") + { + m_last_error = status; + m_running.Set(false); + return; + } + + Common::SleepCurrentThread(1000 * 5); + } +} + +bool NetPlayIndex::Add(NetPlaySession session) +{ + m_running.Set(true); + + Common::HttpRequest request; + auto response = request.Get( + Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/session/add?name=" + + request.EscapeComponent(session.name) + "®ion=" + request.EscapeComponent(session.region) + + "&game=" + request.EscapeComponent(session.game_id) + + "&password=" + std::to_string(session.has_password) + "&method=" + session.method + + "&server_id=" + session.server_id + "&in_game=" + std::to_string(session.in_game) + + "&port=" + std::to_string(session.port) + + "&player_count=" + std::to_string(session.player_count) + "&version=" + Common::scm_desc_str); + + if (!response.has_value()) + { + m_last_error = "NO_RESPONSE"; + return false; + } + + auto json = ParseResponse(response.value()); + + if (!json) + { + m_last_error = "BAD_JSON"; + return false; + } + + std::string status = json->get("status").to_str(); + + if (status != "OK") + { + m_last_error = status; + return false; + } + + m_secret = json->get("secret").to_str(); + m_in_game = session.in_game; + m_player_count = session.player_count; + m_game = session.game_id; + + m_session_thread = std::thread([this] { NotificationLoop(); }); + + m_session_thread.detach(); + + return true; +} + +void NetPlayIndex::SetInGame(bool in_game) +{ + m_in_game = in_game; +} + +void NetPlayIndex::SetPlayerCount(int player_count) +{ + m_player_count = player_count; +} + +void NetPlayIndex::SetGame(const std::string& game) +{ + m_game = game; +} + +void NetPlayIndex::Remove() +{ + if (m_secret.empty()) + return; + + m_running.Set(false); + + if (m_session_thread.joinable()) + m_session_thread.join(); + + // We don't really care whether this fails or not + Common::HttpRequest request; + request.Get(Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/session/remove?secret=" + m_secret); + + m_secret = ""; +} + +std::vector NetPlayIndex::GetRegions() +{ + static std::vector regions{"AF", "CN", "EA", "EU", "NA", "OC"}; + + return regions; +} + +// This encryption system uses simple XOR operations and a checksum +// It isn't very secure but is preferable to adding another dependency on mbedtls +// The encrypted data is encoded as nibbles with the character 'A' as the base offset + +bool NetPlaySession::EncryptID(const std::string& password) +{ + if (password.empty()) + return false; + + u8 i = 0; + + std::string to_encrypt = server_id; + + // Calculate and append checksum to ID + u8 sum = 0; + + for (char c : to_encrypt) + sum += c; + + to_encrypt += sum; + + std::string encrypted_id; + + for (const char& byte : to_encrypt) + { + char c = byte ^ password[i % password.size()]; + c += i; + encrypted_id += 'A' + ((c & 0xF0) >> 4); + encrypted_id += 'A' + (c & 0x0F); + ++i; + } + + server_id = encrypted_id; + + return true; +} + +std::optional NetPlaySession::DecryptID(const std::string& password) const +{ + if (password.empty()) + return {}; + + // If the length of an encrypted session id is not divisble by two, it's invalid + if (server_id.empty() || server_id.size() % 2 != 0) + return {}; + + std::string decoded; + + for (size_t i = 0; i < server_id.size(); i += 2) + { + char c = (server_id[i] - 'A') << 4 | (server_id[i + 1] - 'A'); + decoded.push_back(c); + } + + u8 i = 0; + for (auto& c : decoded) + { + c -= i; + c ^= password[i % password.size()]; + ++i; + } + + // Verify checksum + u8 expected_sum = decoded[decoded.size() - 1]; + + decoded = decoded.substr(0, decoded.size() - 1); + + u8 sum = 0; + for (char c : decoded) + sum += c; + + if (sum != expected_sum) + return {}; + + return decoded; +} + +const std::string& NetPlayIndex::GetLastError() const +{ + return m_last_error; +} diff --git a/Source/Core/UICommon/NetPlayIndex.h b/Source/Core/UICommon/NetPlayIndex.h new file mode 100644 index 0000000000..929e163eb2 --- /dev/null +++ b/Source/Core/UICommon/NetPlayIndex.h @@ -0,0 +1,65 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include + +#include "Common/Flag.h" + +struct NetPlaySession +{ + std::string name; + std::string region; + std::string method; + std::string server_id; + std::string game_id; + + int player_count; + int port; + + bool has_password; + bool in_game; + + bool EncryptID(const std::string& password); + std::optional DecryptID(const std::string& password) const; +}; + +class NetPlayIndex +{ +public: + explicit NetPlayIndex(); + ~NetPlayIndex(); + + std::optional> + List(const std::map& filters = {}); + + static std::vector GetRegions(); + + bool Add(NetPlaySession session); + void Remove(); + + void SetPlayerCount(int player_count); + void SetInGame(bool in_game); + void SetGame(const std::string& game); + + const std::string& GetLastError() const; + +private: + void NotificationLoop(); + + Common::Flag m_running; + + std::string m_secret; + std::string m_game; + int m_player_count = 0; + bool m_in_game = false; + + std::string m_last_error; + std::thread m_session_thread; +}; diff --git a/Source/Core/UICommon/UICommon.vcxproj b/Source/Core/UICommon/UICommon.vcxproj index b669d9b195..7526d92b0b 100644 --- a/Source/Core/UICommon/UICommon.vcxproj +++ b/Source/Core/UICommon/UICommon.vcxproj @@ -53,6 +53,7 @@ + @@ -69,6 +70,7 @@ + @@ -86,4 +88,4 @@ - \ No newline at end of file +