2019-03-30 13:48:46 +00:00
|
|
|
// Copyright 2019 Dolphin Emulator Project
|
|
|
|
// Licensed under GPLv2+
|
|
|
|
// Refer to the license.txt file included.
|
|
|
|
|
|
|
|
#include "UICommon/NetPlayIndex.h"
|
|
|
|
|
2019-04-06 12:09:20 +00:00
|
|
|
#include <chrono>
|
2019-03-30 13:50:57 +00:00
|
|
|
#include <numeric>
|
|
|
|
#include <string>
|
|
|
|
|
2019-03-30 13:48:46 +00:00
|
|
|
#include <picojson/picojson.h>
|
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
#include "Common/Common.h"
|
2019-03-30 13:48:46 +00:00
|
|
|
#include "Common/HttpRequest.h"
|
|
|
|
#include "Common/Thread.h"
|
|
|
|
#include "Common/Version.h"
|
|
|
|
|
|
|
|
#include "Core/Config/NetplaySettings.h"
|
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
NetPlayIndex::NetPlayIndex() = default;
|
2019-03-30 13:48:46 +00:00
|
|
|
|
|
|
|
NetPlayIndex::~NetPlayIndex()
|
|
|
|
{
|
|
|
|
if (!m_secret.empty())
|
|
|
|
Remove();
|
|
|
|
}
|
|
|
|
|
|
|
|
static std::optional<picojson::value> ParseResponse(std::vector<u8> response)
|
|
|
|
{
|
|
|
|
std::string response_string(reinterpret_cast<char*>(response.data()), response.size());
|
|
|
|
|
|
|
|
picojson::value json;
|
|
|
|
|
|
|
|
auto error = picojson::parse(json, response_string);
|
|
|
|
|
|
|
|
if (!error.empty())
|
|
|
|
return {};
|
|
|
|
|
|
|
|
return json;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::optional<std::vector<NetPlaySession>>
|
|
|
|
NetPlayIndex::List(const std::map<std::string, std::string>& filters)
|
|
|
|
{
|
|
|
|
Common::HttpRequest request;
|
|
|
|
|
|
|
|
std::string list_url = Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/list";
|
|
|
|
|
|
|
|
if (!filters.empty())
|
|
|
|
{
|
2019-03-30 13:50:57 +00:00
|
|
|
list_url += '?';
|
2019-03-30 13:48:46 +00:00
|
|
|
for (const auto& filter : filters)
|
|
|
|
{
|
2019-03-30 13:50:57 +00:00
|
|
|
list_url += filter.first + '=' + request.EscapeComponent(filter.second) + '&';
|
2019-03-30 13:48:46 +00:00
|
|
|
}
|
2019-03-30 13:50:57 +00:00
|
|
|
list_url.pop_back();
|
2019-03-30 13:48:46 +00:00
|
|
|
}
|
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
auto response = request.Get(list_url, {{"X-Is-Dolphin", "1"}});
|
2019-03-30 13:48:46 +00:00
|
|
|
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<NetPlaySession> sessions;
|
|
|
|
|
|
|
|
for (const auto& entry : entries.get<picojson::array>())
|
|
|
|
{
|
|
|
|
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");
|
2019-04-07 21:53:13 +00:00
|
|
|
const auto& version = entry.get("version");
|
2019-03-30 13:48:46 +00:00
|
|
|
|
|
|
|
if (!name.is<std::string>() || !region.is<std::string>() || !method.is<std::string>() ||
|
|
|
|
!server_id.is<std::string>() || !game_id.is<std::string>() || !has_password.is<bool>() ||
|
2019-04-07 21:53:13 +00:00
|
|
|
!player_count.is<double>() || !port.is<double>() || !in_game.is<bool>() ||
|
|
|
|
!version.is<std::string>())
|
2019-03-30 13:50:57 +00:00
|
|
|
{
|
2019-03-30 13:48:46 +00:00
|
|
|
continue;
|
2019-03-30 13:50:57 +00:00
|
|
|
}
|
2019-03-30 13:48:46 +00:00
|
|
|
|
|
|
|
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();
|
2019-04-07 21:53:13 +00:00
|
|
|
session.version = version.to_str();
|
2019-03-30 13:48:46 +00:00
|
|
|
session.has_password = has_password.get<bool>();
|
|
|
|
session.player_count = static_cast<int>(player_count.get<double>());
|
|
|
|
session.port = static_cast<int>(port.get<double>());
|
|
|
|
session.in_game = in_game.get<bool>();
|
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
sessions.push_back(std::move(session));
|
2019-03-30 13:48:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return sessions;
|
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayIndex::NotificationLoop()
|
|
|
|
{
|
2019-04-06 12:09:20 +00:00
|
|
|
while (!m_session_thread_exit_event.WaitFor(std::chrono::seconds(5)))
|
2019-03-30 13:48:46 +00:00
|
|
|
{
|
|
|
|
Common::HttpRequest request;
|
|
|
|
auto response = request.Get(
|
|
|
|
Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/session/active?secret=" + m_secret +
|
2019-03-30 13:50:57 +00:00
|
|
|
"&player_count=" + std::to_string(m_player_count) +
|
|
|
|
"&game=" + request.EscapeComponent(m_game) + "&in_game=" + std::to_string(m_in_game),
|
|
|
|
{{"X-Is-Dolphin", "1"}});
|
2019-03-30 13:48:46 +00:00
|
|
|
|
|
|
|
if (!response)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
auto json = ParseResponse(response.value());
|
|
|
|
|
|
|
|
if (!json)
|
|
|
|
{
|
|
|
|
m_last_error = "BAD_JSON";
|
2019-04-10 23:21:40 +00:00
|
|
|
m_secret.clear();
|
|
|
|
m_error_callback();
|
2019-03-30 13:48:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string status = json->get("status").to_str();
|
|
|
|
|
|
|
|
if (status != "OK")
|
|
|
|
{
|
2019-03-30 13:50:57 +00:00
|
|
|
m_last_error = std::move(status);
|
2019-04-10 23:21:40 +00:00
|
|
|
m_secret.clear();
|
|
|
|
m_error_callback();
|
2019-03-30 13:48:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool NetPlayIndex::Add(NetPlaySession session)
|
|
|
|
{
|
|
|
|
Common::HttpRequest request;
|
2019-03-30 13:50:57 +00:00
|
|
|
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,
|
|
|
|
{{"X-Is-Dolphin", "1"}});
|
2019-03-30 13:48:46 +00:00
|
|
|
|
|
|
|
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")
|
|
|
|
{
|
2019-03-30 13:50:57 +00:00
|
|
|
m_last_error = std::move(status);
|
2019-03-30 13:48:46 +00:00
|
|
|
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;
|
|
|
|
|
2019-04-11 02:39:16 +00:00
|
|
|
m_session_thread_exit_event.Set();
|
|
|
|
if (m_session_thread.joinable())
|
|
|
|
m_session_thread.join();
|
|
|
|
m_session_thread_exit_event.Reset();
|
|
|
|
|
2019-03-30 13:48:46 +00:00
|
|
|
m_session_thread = std::thread([this] { NotificationLoop(); });
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayIndex::SetInGame(bool in_game)
|
|
|
|
{
|
|
|
|
m_in_game = in_game;
|
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayIndex::SetPlayerCount(int player_count)
|
|
|
|
{
|
|
|
|
m_player_count = player_count;
|
|
|
|
}
|
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
void NetPlayIndex::SetGame(const std::string game)
|
2019-03-30 13:48:46 +00:00
|
|
|
{
|
2019-03-30 13:50:57 +00:00
|
|
|
m_game = std::move(game);
|
2019-03-30 13:48:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayIndex::Remove()
|
|
|
|
{
|
|
|
|
if (m_secret.empty())
|
|
|
|
return;
|
|
|
|
|
2019-04-06 12:09:20 +00:00
|
|
|
m_session_thread_exit_event.Set();
|
2019-03-30 13:48:46 +00:00
|
|
|
|
|
|
|
if (m_session_thread.joinable())
|
|
|
|
m_session_thread.join();
|
|
|
|
|
|
|
|
// We don't really care whether this fails or not
|
|
|
|
Common::HttpRequest request;
|
2019-03-30 13:50:57 +00:00
|
|
|
request.Get(Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/session/remove?secret=" + m_secret,
|
|
|
|
{{"X-Is-Dolphin", "1"}});
|
2019-03-30 13:48:46 +00:00
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
m_secret.clear();
|
2019-03-30 13:48:46 +00:00
|
|
|
}
|
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
std::vector<std::pair<std::string, std::string>> NetPlayIndex::GetRegions()
|
2019-03-30 13:48:46 +00:00
|
|
|
{
|
2019-03-30 13:50:57 +00:00
|
|
|
return {
|
|
|
|
{"EA", _trans("East Asia")}, {"CN", _trans("China")}, {"EU", _trans("Europe")},
|
|
|
|
{"NA", _trans("North America")}, {"SA", _trans("South America")}, {"OC", _trans("Oceania")},
|
|
|
|
{"AF", _trans("Africa")},
|
|
|
|
};
|
2019-03-30 13:48:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
std::string to_encrypt = server_id;
|
|
|
|
|
|
|
|
// Calculate and append checksum to ID
|
2019-03-30 13:50:57 +00:00
|
|
|
const u8 sum = std::accumulate(to_encrypt.begin(), to_encrypt.end(), u8{0});
|
2019-03-30 13:48:46 +00:00
|
|
|
to_encrypt += sum;
|
|
|
|
|
|
|
|
std::string encrypted_id;
|
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
u8 i = 0;
|
|
|
|
for (const char byte : to_encrypt)
|
2019-03-30 13:48:46 +00:00
|
|
|
{
|
|
|
|
char c = byte ^ password[i % password.size()];
|
|
|
|
c += i;
|
|
|
|
encrypted_id += 'A' + ((c & 0xF0) >> 4);
|
|
|
|
encrypted_id += 'A' + (c & 0x0F);
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
server_id = std::move(encrypted_id);
|
2019-03-30 13:48:46 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::optional<std::string> 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
|
2019-03-30 13:50:57 +00:00
|
|
|
const u8 expected_sum = decoded[decoded.size() - 1];
|
2019-03-30 13:48:46 +00:00
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
decoded.pop_back();
|
2019-03-30 13:48:46 +00:00
|
|
|
|
2019-03-30 13:50:57 +00:00
|
|
|
const u8 sum = std::accumulate(decoded.begin(), decoded.end(), u8{0});
|
2019-03-30 13:48:46 +00:00
|
|
|
|
|
|
|
if (sum != expected_sum)
|
|
|
|
return {};
|
|
|
|
|
|
|
|
return decoded;
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::string& NetPlayIndex::GetLastError() const
|
|
|
|
{
|
|
|
|
return m_last_error;
|
|
|
|
}
|
2019-04-10 23:21:40 +00:00
|
|
|
|
|
|
|
bool NetPlayIndex::HasActiveSession() const
|
|
|
|
{
|
|
|
|
return !m_secret.empty();
|
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayIndex::SetErrorCallback(std::function<void()> function)
|
|
|
|
{
|
|
|
|
m_error_callback = function;
|
|
|
|
}
|