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 "®ion=" + 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