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