NetPlayIndex.cpp 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. // Copyright 2019 Dolphin Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include "UICommon/NetPlayIndex.h"
  4. #include <chrono>
  5. #include <numeric>
  6. #include <string>
  7. #include <picojson.h>
  8. #include "Common/Common.h"
  9. #include "Common/HttpRequest.h"
  10. #include "Common/Thread.h"
  11. #include "Common/Version.h"
  12. #include "Core/Config/NetplaySettings.h"
  13. NetPlayIndex::NetPlayIndex() = default;
  14. NetPlayIndex::~NetPlayIndex()
  15. {
  16. if (!m_secret.empty())
  17. Remove();
  18. }
  19. static std::optional<picojson::value> ParseResponse(const std::vector<u8>& response)
  20. {
  21. const std::string response_string(reinterpret_cast<const char*>(response.data()),
  22. response.size());
  23. picojson::value json;
  24. const auto error = picojson::parse(json, response_string);
  25. if (!error.empty())
  26. return {};
  27. return json;
  28. }
  29. std::optional<std::vector<NetPlaySession>>
  30. NetPlayIndex::List(const std::map<std::string, std::string>& filters)
  31. {
  32. Common::HttpRequest request;
  33. std::string list_url = Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/list";
  34. if (!filters.empty())
  35. {
  36. list_url += '?';
  37. for (const auto& filter : filters)
  38. {
  39. list_url += filter.first + '=' + request.EscapeComponent(filter.second) + '&';
  40. }
  41. list_url.pop_back();
  42. }
  43. auto response =
  44. request.Get(list_url, {{"X-Is-Dolphin", "1"}}, Common::HttpRequest::AllowedReturnCodes::All);
  45. if (!response)
  46. {
  47. m_last_error = "NO_RESPONSE";
  48. return {};
  49. }
  50. auto json = ParseResponse(response.value());
  51. if (!json)
  52. {
  53. m_last_error = "BAD_JSON";
  54. return {};
  55. }
  56. const auto& status = json->get("status");
  57. if (status.to_str() != "OK")
  58. {
  59. m_last_error = status.to_str();
  60. return {};
  61. }
  62. const auto& entries = json->get("sessions");
  63. std::vector<NetPlaySession> sessions;
  64. for (const auto& entry : entries.get<picojson::array>())
  65. {
  66. const auto& name = entry.get("name");
  67. const auto& region = entry.get("region");
  68. const auto& method = entry.get("method");
  69. const auto& game_id = entry.get("game");
  70. const auto& server_id = entry.get("server_id");
  71. const auto& has_password = entry.get("password");
  72. const auto& player_count = entry.get("player_count");
  73. const auto& port = entry.get("port");
  74. const auto& in_game = entry.get("in_game");
  75. const auto& version = entry.get("version");
  76. if (!name.is<std::string>() || !region.is<std::string>() || !method.is<std::string>() ||
  77. !server_id.is<std::string>() || !game_id.is<std::string>() || !has_password.is<bool>() ||
  78. !player_count.is<double>() || !port.is<double>() || !in_game.is<bool>() ||
  79. !version.is<std::string>())
  80. {
  81. continue;
  82. }
  83. NetPlaySession session;
  84. session.name = name.to_str();
  85. session.region = region.to_str();
  86. session.game_id = game_id.to_str();
  87. session.server_id = server_id.to_str();
  88. session.method = method.to_str();
  89. session.version = version.to_str();
  90. session.has_password = has_password.get<bool>();
  91. session.player_count = static_cast<int>(player_count.get<double>());
  92. session.port = static_cast<int>(port.get<double>());
  93. session.in_game = in_game.get<bool>();
  94. sessions.push_back(std::move(session));
  95. }
  96. return sessions;
  97. }
  98. void NetPlayIndex::NotificationLoop()
  99. {
  100. while (!m_session_thread_exit_event.WaitFor(std::chrono::seconds(5)))
  101. {
  102. Common::HttpRequest request;
  103. auto response = request.Get(
  104. Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/session/active?secret=" + m_secret +
  105. "&player_count=" + std::to_string(m_player_count) +
  106. "&game=" + request.EscapeComponent(m_game) + "&in_game=" + std::to_string(m_in_game),
  107. {{"X-Is-Dolphin", "1"}}, Common::HttpRequest::AllowedReturnCodes::All);
  108. if (!response)
  109. continue;
  110. auto json = ParseResponse(response.value());
  111. if (!json)
  112. {
  113. m_last_error = "BAD_JSON";
  114. m_secret.clear();
  115. m_error_callback();
  116. return;
  117. }
  118. std::string status = json->get("status").to_str();
  119. if (status != "OK")
  120. {
  121. m_last_error = std::move(status);
  122. m_secret.clear();
  123. m_error_callback();
  124. return;
  125. }
  126. }
  127. }
  128. bool NetPlayIndex::Add(const NetPlaySession& session)
  129. {
  130. Common::HttpRequest request;
  131. auto response = request.Get(
  132. Config::Get(Config::NETPLAY_INDEX_URL) +
  133. "/v0/session/add?name=" + request.EscapeComponent(session.name) +
  134. "&region=" + request.EscapeComponent(session.region) +
  135. "&game=" + request.EscapeComponent(session.game_id) +
  136. "&password=" + std::to_string(session.has_password) + "&method=" + session.method +
  137. "&server_id=" + session.server_id + "&in_game=" + std::to_string(session.in_game) +
  138. "&port=" + std::to_string(session.port) + "&player_count=" +
  139. std::to_string(session.player_count) + "&version=" + Common::GetScmDescStr(),
  140. {{"X-Is-Dolphin", "1"}}, Common::HttpRequest::AllowedReturnCodes::All);
  141. if (!response.has_value())
  142. {
  143. m_last_error = "NO_RESPONSE";
  144. return false;
  145. }
  146. auto json = ParseResponse(response.value());
  147. if (!json)
  148. {
  149. m_last_error = "BAD_JSON";
  150. return false;
  151. }
  152. std::string status = json->get("status").to_str();
  153. if (status != "OK")
  154. {
  155. m_last_error = std::move(status);
  156. return false;
  157. }
  158. m_secret = json->get("secret").to_str();
  159. m_in_game = session.in_game;
  160. m_player_count = session.player_count;
  161. m_game = session.game_id;
  162. m_session_thread_exit_event.Set();
  163. if (m_session_thread.joinable())
  164. m_session_thread.join();
  165. m_session_thread_exit_event.Reset();
  166. m_session_thread = std::thread([this] { NotificationLoop(); });
  167. return true;
  168. }
  169. void NetPlayIndex::SetInGame(bool in_game)
  170. {
  171. m_in_game = in_game;
  172. }
  173. void NetPlayIndex::SetPlayerCount(int player_count)
  174. {
  175. m_player_count = player_count;
  176. }
  177. void NetPlayIndex::SetGame(std::string game)
  178. {
  179. m_game = std::move(game);
  180. }
  181. void NetPlayIndex::Remove()
  182. {
  183. if (m_secret.empty())
  184. return;
  185. m_session_thread_exit_event.Set();
  186. if (m_session_thread.joinable())
  187. m_session_thread.join();
  188. // We don't really care whether this fails or not
  189. Common::HttpRequest request;
  190. request.Get(Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/session/remove?secret=" + m_secret,
  191. {{"X-Is-Dolphin", "1"}}, Common::HttpRequest::AllowedReturnCodes::All);
  192. m_secret.clear();
  193. }
  194. std::vector<std::pair<std::string, std::string>> NetPlayIndex::GetRegions()
  195. {
  196. return {
  197. {"EA", _trans("East Asia")}, {"CN", _trans("China")}, {"EU", _trans("Europe")},
  198. {"NA", _trans("North America")}, {"SA", _trans("South America")}, {"OC", _trans("Oceania")},
  199. {"AF", _trans("Africa")},
  200. };
  201. }
  202. // This encryption system uses simple XOR operations and a checksum
  203. // It isn't very secure but is preferable to adding another dependency on mbedtls
  204. // The encrypted data is encoded as nibbles with the character 'A' as the base offset
  205. bool NetPlaySession::EncryptID(std::string_view password)
  206. {
  207. if (password.empty())
  208. return false;
  209. std::string to_encrypt = server_id;
  210. // Calculate and append checksum to ID
  211. const u8 sum = std::accumulate(to_encrypt.begin(), to_encrypt.end(), u8{0});
  212. to_encrypt += sum;
  213. std::string encrypted_id;
  214. u8 i = 0;
  215. for (const char byte : to_encrypt)
  216. {
  217. char c = byte ^ password[i % password.size()];
  218. c += i;
  219. encrypted_id += 'A' + ((c & 0xF0) >> 4);
  220. encrypted_id += 'A' + (c & 0x0F);
  221. ++i;
  222. }
  223. server_id = std::move(encrypted_id);
  224. return true;
  225. }
  226. std::optional<std::string> NetPlaySession::DecryptID(std::string_view password) const
  227. {
  228. if (password.empty())
  229. return {};
  230. // If the length of an encrypted session id is not divisble by two, it's invalid
  231. if (server_id.empty() || server_id.size() % 2 != 0)
  232. return {};
  233. std::string decoded;
  234. for (size_t i = 0; i < server_id.size(); i += 2)
  235. {
  236. char c = (server_id[i] - 'A') << 4 | (server_id[i + 1] - 'A');
  237. decoded.push_back(c);
  238. }
  239. u8 i = 0;
  240. for (auto& c : decoded)
  241. {
  242. c -= i;
  243. c ^= password[i % password.size()];
  244. ++i;
  245. }
  246. // Verify checksum
  247. const u8 expected_sum = decoded[decoded.size() - 1];
  248. decoded.pop_back();
  249. const u8 sum = std::accumulate(decoded.begin(), decoded.end(), u8{0});
  250. if (sum != expected_sum)
  251. return {};
  252. return decoded;
  253. }
  254. const std::string& NetPlayIndex::GetLastError() const
  255. {
  256. return m_last_error;
  257. }
  258. bool NetPlayIndex::HasActiveSession() const
  259. {
  260. return !m_secret.empty();
  261. }
  262. void NetPlayIndex::SetErrorCallback(std::function<void()> callback)
  263. {
  264. m_error_callback = std::move(callback);
  265. }