Merge pull request #4211 from wwylele/web-cleanup
web_service: stop using std::future + callback style async
This commit is contained in:
commit
4a30a502a0
23 changed files with 333 additions and 458 deletions
|
@ -26,6 +26,7 @@
|
||||||
#include "citra/config.h"
|
#include "citra/config.h"
|
||||||
#include "citra/emu_window/emu_window_sdl2.h"
|
#include "citra/emu_window/emu_window_sdl2.h"
|
||||||
#include "common/common_paths.h"
|
#include "common/common_paths.h"
|
||||||
|
#include "common/detached_tasks.h"
|
||||||
#include "common/file_util.h"
|
#include "common/file_util.h"
|
||||||
#include "common/logging/backend.h"
|
#include "common/logging/backend.h"
|
||||||
#include "common/logging/filter.h"
|
#include "common/logging/filter.h"
|
||||||
|
@ -129,6 +130,7 @@ static void InitializeLogging() {
|
||||||
|
|
||||||
/// Application entry point
|
/// Application entry point
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
|
Common::DetachedTasks detached_tasks;
|
||||||
Config config;
|
Config config;
|
||||||
int option_index = 0;
|
int option_index = 0;
|
||||||
bool use_gdbstub = Settings::values.use_gdbstub;
|
bool use_gdbstub = Settings::values.use_gdbstub;
|
||||||
|
@ -344,5 +346,6 @@ int main(int argc, char** argv) {
|
||||||
|
|
||||||
Core::Movie::GetInstance().Shutdown();
|
Core::Movie::GetInstance().Shutdown();
|
||||||
|
|
||||||
|
detached_tasks.WaitForAllTasks();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
#include "citra_qt/configuration/configure_web.h"
|
#include "citra_qt/configuration/configure_web.h"
|
||||||
#include "citra_qt/ui_settings.h"
|
#include "citra_qt/ui_settings.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
|
@ -16,7 +17,7 @@ ConfigureWeb::ConfigureWeb(QWidget* parent)
|
||||||
connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, this,
|
connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, this,
|
||||||
&ConfigureWeb::RefreshTelemetryID);
|
&ConfigureWeb::RefreshTelemetryID);
|
||||||
connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin);
|
connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin);
|
||||||
connect(this, &ConfigureWeb::LoginVerified, this, &ConfigureWeb::OnLoginVerified);
|
connect(&verify_watcher, &QFutureWatcher<bool>::finished, this, &ConfigureWeb::OnLoginVerified);
|
||||||
|
|
||||||
#ifndef USE_DISCORD_PRESENCE
|
#ifndef USE_DISCORD_PRESENCE
|
||||||
ui->discord_group->setVisible(false);
|
ui->discord_group->setVisible(false);
|
||||||
|
@ -89,17 +90,19 @@ void ConfigureWeb::OnLoginChanged() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureWeb::VerifyLogin() {
|
void ConfigureWeb::VerifyLogin() {
|
||||||
verified =
|
|
||||||
Core::VerifyLogin(ui->edit_username->text().toStdString(),
|
|
||||||
ui->edit_token->text().toStdString(), [&]() { emit LoginVerified(); });
|
|
||||||
ui->button_verify_login->setDisabled(true);
|
ui->button_verify_login->setDisabled(true);
|
||||||
ui->button_verify_login->setText(tr("Verifying"));
|
ui->button_verify_login->setText(tr("Verifying"));
|
||||||
|
verify_watcher.setFuture(
|
||||||
|
QtConcurrent::run([this, username = ui->edit_username->text().toStdString(),
|
||||||
|
token = ui->edit_token->text().toStdString()]() {
|
||||||
|
return Core::VerifyLogin(username, token);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureWeb::OnLoginVerified() {
|
void ConfigureWeb::OnLoginVerified() {
|
||||||
ui->button_verify_login->setEnabled(true);
|
ui->button_verify_login->setEnabled(true);
|
||||||
ui->button_verify_login->setText(tr("Verify"));
|
ui->button_verify_login->setText(tr("Verify"));
|
||||||
if (verified.get()) {
|
if (verify_watcher.result()) {
|
||||||
user_verified = true;
|
user_verified = true;
|
||||||
ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16));
|
ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16));
|
||||||
ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16));
|
ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16));
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <future>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <QFutureWatcher>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
|
@ -28,14 +28,11 @@ public slots:
|
||||||
void VerifyLogin();
|
void VerifyLogin();
|
||||||
void OnLoginVerified();
|
void OnLoginVerified();
|
||||||
|
|
||||||
signals:
|
|
||||||
void LoginVerified();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setConfiguration();
|
void setConfiguration();
|
||||||
|
|
||||||
bool user_verified = true;
|
bool user_verified = true;
|
||||||
std::future<bool> verified;
|
QFutureWatcher<bool> verify_watcher;
|
||||||
|
|
||||||
std::unique_ptr<Ui::ConfigureWeb> ui;
|
std::unique_ptr<Ui::ConfigureWeb> ui;
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
#include "citra_qt/updater/updater.h"
|
#include "citra_qt/updater/updater.h"
|
||||||
#include "citra_qt/util/clickable_label.h"
|
#include "citra_qt/util/clickable_label.h"
|
||||||
#include "common/common_paths.h"
|
#include "common/common_paths.h"
|
||||||
|
#include "common/detached_tasks.h"
|
||||||
#include "common/logging/backend.h"
|
#include "common/logging/backend.h"
|
||||||
#include "common/logging/filter.h"
|
#include "common/logging/filter.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
@ -1666,6 +1667,7 @@ void GMainWindow::SetDiscordEnabled(bool state) {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
|
Common::DetachedTasks detached_tasks;
|
||||||
MicroProfileOnThreadCreate("Frontend");
|
MicroProfileOnThreadCreate("Frontend");
|
||||||
SCOPE_EXIT({ MicroProfileShutdown(); });
|
SCOPE_EXIT({ MicroProfileShutdown(); });
|
||||||
|
|
||||||
|
@ -1691,5 +1693,7 @@ int main(int argc, char* argv[]) {
|
||||||
Frontend::RegisterSoftwareKeyboard(std::make_shared<QtKeyboard>(main_window));
|
Frontend::RegisterSoftwareKeyboard(std::make_shared<QtKeyboard>(main_window));
|
||||||
|
|
||||||
main_window.show();
|
main_window.show();
|
||||||
return app.exec();
|
int result = app.exec();
|
||||||
|
detached_tasks.WaitForAllTasks();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,8 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
|
||||||
connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom);
|
connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom);
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
connect(this, &Lobby::LobbyRefreshed, this, &Lobby::OnRefreshLobby);
|
connect(&room_list_watcher, &QFutureWatcher<AnnounceMultiplayerRoom::RoomList>::finished, this,
|
||||||
|
&Lobby::OnRefreshLobby);
|
||||||
|
|
||||||
// manually start a refresh when the window is opening
|
// manually start a refresh when the window is opening
|
||||||
// TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as
|
// TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as
|
||||||
|
@ -161,16 +162,17 @@ void Lobby::ResetModel() {
|
||||||
void Lobby::RefreshLobby() {
|
void Lobby::RefreshLobby() {
|
||||||
if (auto session = announce_multiplayer_session.lock()) {
|
if (auto session = announce_multiplayer_session.lock()) {
|
||||||
ResetModel();
|
ResetModel();
|
||||||
room_list_future = session->GetRoomList([&]() { emit LobbyRefreshed(); });
|
|
||||||
ui->refresh_list->setEnabled(false);
|
ui->refresh_list->setEnabled(false);
|
||||||
ui->refresh_list->setText(tr("Refreshing"));
|
ui->refresh_list->setText(tr("Refreshing"));
|
||||||
|
room_list_watcher.setFuture(
|
||||||
|
QtConcurrent::run([session]() { return session->GetRoomList(); }));
|
||||||
} else {
|
} else {
|
||||||
// TODO(jroweboy): Display an error box about announce couldn't be started
|
// TODO(jroweboy): Display an error box about announce couldn't be started
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Lobby::OnRefreshLobby() {
|
void Lobby::OnRefreshLobby() {
|
||||||
AnnounceMultiplayerRoom::RoomList new_room_list = room_list_future.get();
|
AnnounceMultiplayerRoom::RoomList new_room_list = room_list_watcher.result();
|
||||||
for (auto room : new_room_list) {
|
for (auto room : new_room_list) {
|
||||||
// find the icon for the game if this person owns that game.
|
// find the icon for the game if this person owns that game.
|
||||||
QPixmap smdh_icon;
|
QPixmap smdh_icon;
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <future>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
|
@ -61,11 +60,6 @@ private slots:
|
||||||
void OnJoinRoom(const QModelIndex&);
|
void OnJoinRoom(const QModelIndex&);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/**
|
|
||||||
* Signalled when the latest lobby data is retrieved.
|
|
||||||
*/
|
|
||||||
void LobbyRefreshed();
|
|
||||||
|
|
||||||
void StateChanged(const Network::RoomMember::State&);
|
void StateChanged(const Network::RoomMember::State&);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -84,7 +78,7 @@ private:
|
||||||
QStandardItemModel* game_list;
|
QStandardItemModel* game_list;
|
||||||
LobbyFilterProxyModel* proxy;
|
LobbyFilterProxyModel* proxy;
|
||||||
|
|
||||||
std::future<AnnounceMultiplayerRoom::RoomList> room_list_future;
|
QFutureWatcher<AnnounceMultiplayerRoom::RoomList> room_list_watcher;
|
||||||
std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
|
std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
|
||||||
std::unique_ptr<Ui::Lobby> ui;
|
std::unique_ptr<Ui::Lobby> ui;
|
||||||
QFutureWatcher<void>* watcher;
|
QFutureWatcher<void>* watcher;
|
||||||
|
|
|
@ -42,6 +42,8 @@ add_library(common STATIC
|
||||||
alignment.h
|
alignment.h
|
||||||
announce_multiplayer_room.h
|
announce_multiplayer_room.h
|
||||||
assert.h
|
assert.h
|
||||||
|
detached_tasks.cpp
|
||||||
|
detached_tasks.h
|
||||||
bit_field.h
|
bit_field.h
|
||||||
bit_set.h
|
bit_set.h
|
||||||
chunk_file.h
|
chunk_file.h
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <future>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
@ -90,7 +89,7 @@ public:
|
||||||
* Send the data to the announce service
|
* Send the data to the announce service
|
||||||
* @result The result of the announce attempt
|
* @result The result of the announce attempt
|
||||||
*/
|
*/
|
||||||
virtual std::future<Common::WebResult> Announce() = 0;
|
virtual Common::WebResult Announce() = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empties the stored players
|
* Empties the stored players
|
||||||
|
@ -99,11 +98,9 @@ public:
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the room information from the announce service
|
* Get the room information from the announce service
|
||||||
* @param func a function that gets exectued when the get finished.
|
|
||||||
* Can be used as a callback
|
|
||||||
* @result A list of all rooms the announce service has
|
* @result A list of all rooms the announce service has
|
||||||
*/
|
*/
|
||||||
virtual std::future<RoomList> GetRoomList(std::function<void()> func) = 0;
|
virtual RoomList GetRoomList() = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a delete message to the announce service
|
* Sends a delete message to the announce service
|
||||||
|
@ -124,18 +121,12 @@ public:
|
||||||
const u64 /*preferred_game_id*/) override {}
|
const u64 /*preferred_game_id*/) override {}
|
||||||
void AddPlayer(const std::string& /*nickname*/, const MacAddress& /*mac_address*/,
|
void AddPlayer(const std::string& /*nickname*/, const MacAddress& /*mac_address*/,
|
||||||
const u64 /*game_id*/, const std::string& /*game_name*/) override {}
|
const u64 /*game_id*/, const std::string& /*game_name*/) override {}
|
||||||
std::future<Common::WebResult> Announce() override {
|
Common::WebResult Announce() override {
|
||||||
return std::async(std::launch::deferred, []() {
|
return Common::WebResult{Common::WebResult::Code::NoWebservice, "WebService is missing"};
|
||||||
return Common::WebResult{Common::WebResult::Code::NoWebservice,
|
|
||||||
"WebService is missing"};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
void ClearPlayers() override {}
|
void ClearPlayers() override {}
|
||||||
std::future<RoomList> GetRoomList(std::function<void()> func) override {
|
RoomList GetRoomList() override {
|
||||||
return std::async(std::launch::deferred, [func]() {
|
return RoomList{};
|
||||||
func();
|
|
||||||
return RoomList{};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Delete() override {}
|
void Delete() override {}
|
||||||
|
|
41
src/common/detached_tasks.cpp
Normal file
41
src/common/detached_tasks.cpp
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/detached_tasks.h"
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
DetachedTasks* DetachedTasks::instance = nullptr;
|
||||||
|
|
||||||
|
DetachedTasks::DetachedTasks() {
|
||||||
|
ASSERT(instance == nullptr);
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DetachedTasks::WaitForAllTasks() {
|
||||||
|
std::unique_lock<std::mutex> lock(mutex);
|
||||||
|
cv.wait(lock, [this]() { return count == 0; });
|
||||||
|
}
|
||||||
|
|
||||||
|
DetachedTasks::~DetachedTasks() {
|
||||||
|
std::unique_lock<std::mutex> lock(mutex);
|
||||||
|
ASSERT(count == 0);
|
||||||
|
instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DetachedTasks::AddTask(std::function<void()> task) {
|
||||||
|
std::unique_lock<std::mutex> lock(instance->mutex);
|
||||||
|
++instance->count;
|
||||||
|
std::thread([task{std::move(task)}]() {
|
||||||
|
task();
|
||||||
|
std::unique_lock<std::mutex> lock(instance->mutex);
|
||||||
|
--instance->count;
|
||||||
|
std::notify_all_at_thread_exit(instance->cv, std::move(lock));
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
39
src/common/detached_tasks.h
Normal file
39
src/common/detached_tasks.h
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A background manager which ensures that all detached task is finished before program exits.
|
||||||
|
*
|
||||||
|
* Some tasks, telemetry submission for example, prefer executing asynchronously and don't care
|
||||||
|
* about the result. These tasks are suitable for std::thread::detach(). However, this is unsafe if
|
||||||
|
* the task is launched just before the program exits (which is a common case for telemetry), so we
|
||||||
|
* need to block on these tasks on program exit.
|
||||||
|
*
|
||||||
|
* To make detached task safe, a single DetachedTasks object should be placed in the main(), and
|
||||||
|
* call WaitForAllTasks() after all program execution but before global/static variable destruction.
|
||||||
|
* Any potentially unsafe detached task should be executed via DetachedTasks::AddTask.
|
||||||
|
*/
|
||||||
|
class DetachedTasks {
|
||||||
|
public:
|
||||||
|
DetachedTasks();
|
||||||
|
~DetachedTasks();
|
||||||
|
void WaitForAllTasks();
|
||||||
|
|
||||||
|
static void AddTask(std::function<void()> task);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static DetachedTasks* instance;
|
||||||
|
|
||||||
|
std::condition_variable cv;
|
||||||
|
std::mutex mutex;
|
||||||
|
int count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Common
|
|
@ -21,7 +21,7 @@ static constexpr std::chrono::seconds announce_time_interval(15);
|
||||||
|
|
||||||
AnnounceMultiplayerSession::AnnounceMultiplayerSession() {
|
AnnounceMultiplayerSession::AnnounceMultiplayerSession() {
|
||||||
#ifdef ENABLE_WEB_SERVICE
|
#ifdef ENABLE_WEB_SERVICE
|
||||||
backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url + "/lobby",
|
backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url,
|
||||||
Settings::values.citra_username,
|
Settings::values.citra_username,
|
||||||
Settings::values.citra_token);
|
Settings::values.citra_token);
|
||||||
#else
|
#else
|
||||||
|
@ -87,22 +87,18 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() {
|
||||||
backend->AddPlayer(member.nickname, member.mac_address, member.game_info.id,
|
backend->AddPlayer(member.nickname, member.mac_address, member.game_info.id,
|
||||||
member.game_info.name);
|
member.game_info.name);
|
||||||
}
|
}
|
||||||
future = backend->Announce();
|
Common::WebResult result = backend->Announce();
|
||||||
if (future.valid()) {
|
if (result.result_code != Common::WebResult::Code::Success) {
|
||||||
Common::WebResult result = future.get();
|
std::lock_guard<std::mutex> lock(callback_mutex);
|
||||||
if (result.result_code != Common::WebResult::Code::Success) {
|
for (auto callback : error_callbacks) {
|
||||||
std::lock_guard<std::mutex> lock(callback_mutex);
|
(*callback)(result);
|
||||||
for (auto callback : error_callbacks) {
|
|
||||||
(*callback)(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::future<AnnounceMultiplayerRoom::RoomList> AnnounceMultiplayerSession::GetRoomList(
|
AnnounceMultiplayerRoom::RoomList AnnounceMultiplayerSession::GetRoomList() {
|
||||||
std::function<void()> func) {
|
return backend->GetRoomList();
|
||||||
return backend->GetRoomList(func);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Core
|
} // namespace Core
|
||||||
|
|
|
@ -54,7 +54,7 @@ public:
|
||||||
* @param func A function that gets executed when the async get finished, e.g. a signal
|
* @param func A function that gets executed when the async get finished, e.g. a signal
|
||||||
* @return a list of rooms received from the web service
|
* @return a list of rooms received from the web service
|
||||||
*/
|
*/
|
||||||
std::future<AnnounceMultiplayerRoom::RoomList> GetRoomList(std::function<void()> func);
|
AnnounceMultiplayerRoom::RoomList GetRoomList();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Common::Event shutdown_event;
|
Common::Event shutdown_event;
|
||||||
|
|
|
@ -82,24 +82,20 @@ u64 RegenerateTelemetryId() {
|
||||||
return new_telemetry_id;
|
return new_telemetry_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func) {
|
bool VerifyLogin(std::string username, std::string token) {
|
||||||
#ifdef ENABLE_WEB_SERVICE
|
#ifdef ENABLE_WEB_SERVICE
|
||||||
return WebService::VerifyLogin(username, token, Settings::values.web_api_url + "/profile",
|
return WebService::VerifyLogin(Settings::values.web_api_url, username, token);
|
||||||
func);
|
|
||||||
#else
|
#else
|
||||||
return std::async(std::launch::async, [func{std::move(func)}]() {
|
return false;
|
||||||
func();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
TelemetrySession::TelemetrySession() {
|
TelemetrySession::TelemetrySession() {
|
||||||
#ifdef ENABLE_WEB_SERVICE
|
#ifdef ENABLE_WEB_SERVICE
|
||||||
if (Settings::values.enable_telemetry) {
|
if (Settings::values.enable_telemetry) {
|
||||||
backend = std::make_unique<WebService::TelemetryJson>(
|
backend = std::make_unique<WebService::TelemetryJson>(Settings::values.web_api_url,
|
||||||
Settings::values.web_api_url + "/telemetry", Settings::values.citra_username,
|
Settings::values.citra_username,
|
||||||
Settings::values.citra_token);
|
Settings::values.citra_token);
|
||||||
} else {
|
} else {
|
||||||
backend = std::make_unique<Telemetry::NullVisitor>();
|
backend = std::make_unique<Telemetry::NullVisitor>();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <future>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include "common/telemetry.h"
|
#include "common/telemetry.h"
|
||||||
|
|
||||||
|
@ -31,6 +30,8 @@ public:
|
||||||
field_collection.AddField(type, name, std::move(value));
|
field_collection.AddField(type, name, std::move(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void FinalizeAsyncJob();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Telemetry::FieldCollection field_collection; ///< Tracks all added fields for the session
|
Telemetry::FieldCollection field_collection; ///< Tracks all added fields for the session
|
||||||
std::unique_ptr<Telemetry::VisitorInterface> backend; ///< Backend interface that logs fields
|
std::unique_ptr<Telemetry::VisitorInterface> backend; ///< Backend interface that logs fields
|
||||||
|
@ -55,6 +56,6 @@ u64 RegenerateTelemetryId();
|
||||||
* @param func A function that gets exectued when the verification is finished
|
* @param func A function that gets exectued when the verification is finished
|
||||||
* @returns Future with bool indicating whether the verification succeeded
|
* @returns Future with bool indicating whether the verification succeeded
|
||||||
*/
|
*/
|
||||||
std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func);
|
bool VerifyLogin(std::string username, std::string token);
|
||||||
|
|
||||||
} // namespace Core
|
} // namespace Core
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
#include "common/detached_tasks.h"
|
||||||
#include "common/scm_rev.h"
|
#include "common/scm_rev.h"
|
||||||
#include "core/announce_multiplayer_session.h"
|
#include "core/announce_multiplayer_session.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
|
@ -54,6 +55,7 @@ static void PrintVersion() {
|
||||||
|
|
||||||
/// Application entry point
|
/// Application entry point
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
|
Common::DetachedTasks detached_tasks;
|
||||||
int option_index = 0;
|
int option_index = 0;
|
||||||
char* endarg;
|
char* endarg;
|
||||||
|
|
||||||
|
@ -204,5 +206,6 @@ int main(int argc, char** argv) {
|
||||||
room->Destroy();
|
room->Destroy();
|
||||||
}
|
}
|
||||||
Network::Shutdown();
|
Network::Shutdown();
|
||||||
|
detached_tasks.WaitForAllTasks();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include <future>
|
#include <future>
|
||||||
|
#include "common/detached_tasks.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "web_service/announce_room_json.h"
|
#include "web_service/announce_room_json.h"
|
||||||
#include "web_service/json.h"
|
#include "web_service/json.h"
|
||||||
|
@ -82,30 +83,31 @@ void RoomJson::AddPlayer(const std::string& nickname,
|
||||||
room.members.push_back(member);
|
room.members.push_back(member);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::future<Common::WebResult> RoomJson::Announce() {
|
Common::WebResult RoomJson::Announce() {
|
||||||
nlohmann::json json = room;
|
nlohmann::json json = room;
|
||||||
return PostJson(endpoint_url, json.dump(), false);
|
return client.PostJson("/lobby", json.dump(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoomJson::ClearPlayers() {
|
void RoomJson::ClearPlayers() {
|
||||||
room.members.clear();
|
room.members.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::future<AnnounceMultiplayerRoom::RoomList> RoomJson::GetRoomList(std::function<void()> func) {
|
AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() {
|
||||||
auto DeSerialize = [func](const std::string& reply) -> AnnounceMultiplayerRoom::RoomList {
|
auto reply = client.GetJson("/lobby", true).returned_data;
|
||||||
nlohmann::json json = nlohmann::json::parse(reply);
|
if (reply.empty()) {
|
||||||
AnnounceMultiplayerRoom::RoomList room_list =
|
return {};
|
||||||
json.at("rooms").get<AnnounceMultiplayerRoom::RoomList>();
|
}
|
||||||
func();
|
return nlohmann::json::parse(reply).at("rooms").get<AnnounceMultiplayerRoom::RoomList>();
|
||||||
return room_list;
|
|
||||||
};
|
|
||||||
return GetJson<AnnounceMultiplayerRoom::RoomList>(DeSerialize, endpoint_url, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoomJson::Delete() {
|
void RoomJson::Delete() {
|
||||||
nlohmann::json json;
|
nlohmann::json json;
|
||||||
json["id"] = room.UID;
|
json["id"] = room.UID;
|
||||||
DeleteJson(endpoint_url, json.dump());
|
Common::DetachedTasks::AddTask(
|
||||||
|
[host{this->host}, username{this->username}, token{this->token}, content{json.dump()}]() {
|
||||||
|
// create a new client here because the this->client might be destroyed.
|
||||||
|
Client{host, username, token}.DeleteJson("/lobby", content, false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace WebService
|
} // namespace WebService
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <future>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "common/announce_multiplayer_room.h"
|
#include "common/announce_multiplayer_room.h"
|
||||||
|
#include "web_service/web_backend.h"
|
||||||
|
|
||||||
namespace WebService {
|
namespace WebService {
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ namespace WebService {
|
||||||
*/
|
*/
|
||||||
class RoomJson : public AnnounceMultiplayerRoom::Backend {
|
class RoomJson : public AnnounceMultiplayerRoom::Backend {
|
||||||
public:
|
public:
|
||||||
RoomJson(const std::string& endpoint_url, const std::string& username, const std::string& token)
|
RoomJson(const std::string& host, const std::string& username, const std::string& token)
|
||||||
: endpoint_url(endpoint_url), username(username), token(token) {}
|
: client(host, username, token), host(host), username(username), token(token) {}
|
||||||
~RoomJson() = default;
|
~RoomJson() = default;
|
||||||
void SetRoomInformation(const std::string& uid, const std::string& name, const u16 port,
|
void SetRoomInformation(const std::string& uid, const std::string& name, const u16 port,
|
||||||
const u32 max_player, const u32 net_version, const bool has_password,
|
const u32 max_player, const u32 net_version, const bool has_password,
|
||||||
|
@ -27,14 +27,15 @@ public:
|
||||||
void AddPlayer(const std::string& nickname,
|
void AddPlayer(const std::string& nickname,
|
||||||
const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id,
|
const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id,
|
||||||
const std::string& game_name) override;
|
const std::string& game_name) override;
|
||||||
std::future<Common::WebResult> Announce() override;
|
Common::WebResult Announce() override;
|
||||||
void ClearPlayers() override;
|
void ClearPlayers() override;
|
||||||
std::future<AnnounceMultiplayerRoom::RoomList> GetRoomList(std::function<void()> func) override;
|
AnnounceMultiplayerRoom::RoomList GetRoomList() override;
|
||||||
void Delete() override;
|
void Delete() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AnnounceMultiplayerRoom::Room room;
|
AnnounceMultiplayerRoom::Room room;
|
||||||
std::string endpoint_url;
|
Client client;
|
||||||
|
std::string host;
|
||||||
std::string username;
|
std::string username;
|
||||||
std::string token;
|
std::string token;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
|
#include "common/detached_tasks.h"
|
||||||
#include "web_service/telemetry_json.h"
|
#include "web_service/telemetry_json.h"
|
||||||
#include "web_service/web_backend.h"
|
#include "web_service/web_backend.h"
|
||||||
|
|
||||||
|
@ -81,8 +83,12 @@ void TelemetryJson::Complete() {
|
||||||
SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig");
|
SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig");
|
||||||
SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem");
|
SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem");
|
||||||
|
|
||||||
|
auto content = TopSection().dump();
|
||||||
// Send the telemetry async but don't handle the errors since they were written to the log
|
// Send the telemetry async but don't handle the errors since they were written to the log
|
||||||
future = PostJson(endpoint_url, TopSection().dump(), true);
|
Common::DetachedTasks::AddTask(
|
||||||
|
[host{this->host}, username{this->username}, token{this->token}, content]() {
|
||||||
|
Client{host, username, token}.PostJson("/telemetry", content, true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace WebService
|
} // namespace WebService
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <future>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "common/announce_multiplayer_room.h"
|
#include "common/announce_multiplayer_room.h"
|
||||||
#include "common/telemetry.h"
|
#include "common/telemetry.h"
|
||||||
|
@ -19,9 +18,8 @@ namespace WebService {
|
||||||
*/
|
*/
|
||||||
class TelemetryJson : public Telemetry::VisitorInterface {
|
class TelemetryJson : public Telemetry::VisitorInterface {
|
||||||
public:
|
public:
|
||||||
TelemetryJson(const std::string& endpoint_url, const std::string& username,
|
TelemetryJson(const std::string& host, const std::string& username, const std::string& token)
|
||||||
const std::string& token)
|
: host(host), username(username), token(token) {}
|
||||||
: endpoint_url(endpoint_url), username(username), token(token) {}
|
|
||||||
~TelemetryJson() = default;
|
~TelemetryJson() = default;
|
||||||
|
|
||||||
void Visit(const Telemetry::Field<bool>& field) override;
|
void Visit(const Telemetry::Field<bool>& field) override;
|
||||||
|
@ -53,10 +51,9 @@ private:
|
||||||
|
|
||||||
nlohmann::json output;
|
nlohmann::json output;
|
||||||
std::array<nlohmann::json, 7> sections;
|
std::array<nlohmann::json, 7> sections;
|
||||||
std::string endpoint_url;
|
std::string host;
|
||||||
std::string username;
|
std::string username;
|
||||||
std::string token;
|
std::string token;
|
||||||
std::future<Common::WebResult> future;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace WebService
|
} // namespace WebService
|
||||||
|
|
|
@ -8,26 +8,20 @@
|
||||||
|
|
||||||
namespace WebService {
|
namespace WebService {
|
||||||
|
|
||||||
std::future<bool> VerifyLogin(std::string& username, std::string& token,
|
bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token) {
|
||||||
const std::string& endpoint_url, std::function<void()> func) {
|
Client client(host, username, token);
|
||||||
auto get_func = [func, username](const std::string& reply) -> bool {
|
auto reply = client.GetJson("/profile", false).returned_data;
|
||||||
func();
|
if (reply.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nlohmann::json json = nlohmann::json::parse(reply);
|
||||||
|
const auto iter = json.find("username");
|
||||||
|
|
||||||
if (reply.empty()) {
|
if (iter == json.end()) {
|
||||||
return false;
|
return username.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json json = nlohmann::json::parse(reply);
|
return username == *iter;
|
||||||
const auto iter = json.find("username");
|
|
||||||
|
|
||||||
if (iter == json.end()) {
|
|
||||||
return username.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
return username == *iter;
|
|
||||||
};
|
|
||||||
UpdateCoreJWT(true, username, token);
|
|
||||||
return GetJson<bool>(get_func, endpoint_url, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace WebService
|
} // namespace WebService
|
||||||
|
|
|
@ -12,13 +12,11 @@ namespace WebService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if username and token is valid
|
* Checks if username and token is valid
|
||||||
|
* @param host the web API URL
|
||||||
* @param username Citra username to use for authentication.
|
* @param username Citra username to use for authentication.
|
||||||
* @param token Citra token to use for authentication.
|
* @param token Citra token to use for authentication.
|
||||||
* @param endpoint_url URL of the services.citra-emu.org endpoint.
|
* @returns a bool indicating whether the verification succeeded
|
||||||
* @param func A function that gets exectued when the verification is finished
|
|
||||||
* @returns Future with bool indicating whether the verification succeeded
|
|
||||||
*/
|
*/
|
||||||
std::future<bool> VerifyLogin(std::string& username, std::string& token,
|
bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token);
|
||||||
const std::string& endpoint_url, std::function<void()> func);
|
|
||||||
|
|
||||||
} // namespace WebService
|
} // namespace WebService
|
||||||
|
|
|
@ -20,334 +20,130 @@ constexpr int HTTPS_PORT = 443;
|
||||||
|
|
||||||
constexpr int TIMEOUT_SECONDS = 30;
|
constexpr int TIMEOUT_SECONDS = 30;
|
||||||
|
|
||||||
std::string UpdateCoreJWT(bool force_new_token, const std::string& username,
|
Client::JWTCache Client::jwt_cache{};
|
||||||
const std::string& token) {
|
|
||||||
static std::string jwt;
|
|
||||||
if (jwt.empty() || force_new_token) {
|
|
||||||
if (!username.empty() && !token.empty()) {
|
|
||||||
std::future<Common::WebResult> future =
|
|
||||||
PostJson(Settings::values.web_api_url + "/jwt/internal", username, token);
|
|
||||||
jwt = future.get().returned_data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return jwt;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<httplib::Client> GetClientFor(const LUrlParser::clParseURL& parsedUrl) {
|
Client::Client(const std::string& host, const std::string& username, const std::string& token)
|
||||||
namespace hl = httplib;
|
: host(host), username(username), token(token) {
|
||||||
|
std::lock_guard<std::mutex> lock(jwt_cache.mutex);
|
||||||
int port;
|
if (username == jwt_cache.username && token == jwt_cache.token) {
|
||||||
|
jwt = jwt_cache.jwt;
|
||||||
std::unique_ptr<hl::Client> cli;
|
|
||||||
|
|
||||||
if (parsedUrl.m_Scheme == "http") {
|
|
||||||
if (!parsedUrl.GetPort(&port)) {
|
|
||||||
port = HTTP_PORT;
|
|
||||||
}
|
|
||||||
return std::make_unique<hl::Client>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS);
|
|
||||||
} else if (parsedUrl.m_Scheme == "https") {
|
|
||||||
if (!parsedUrl.GetPort(&port)) {
|
|
||||||
port = HTTPS_PORT;
|
|
||||||
}
|
|
||||||
return std::make_unique<hl::SSLClient>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS);
|
|
||||||
} else {
|
|
||||||
LOG_ERROR(WebService, "Bad URL scheme {}", parsedUrl.m_Scheme);
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Common::WebResult PostJsonAsyncFn(const std::string& url,
|
Common::WebResult Client::GenericJson(const std::string& method, const std::string& path,
|
||||||
const LUrlParser::clParseURL& parsed_url,
|
const std::string& data, const std::string& jwt,
|
||||||
const httplib::Headers& params, const std::string& data,
|
const std::string& username, const std::string& token) {
|
||||||
bool is_jwt_requested) {
|
|
||||||
static bool is_first_attempt = true;
|
|
||||||
|
|
||||||
namespace hl = httplib;
|
|
||||||
std::unique_ptr<hl::Client> cli = GetClientFor(parsed_url);
|
|
||||||
|
|
||||||
if (cli == nullptr) {
|
if (cli == nullptr) {
|
||||||
return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"};
|
auto parsedUrl = LUrlParser::clParseURL::ParseURL(host);
|
||||||
|
int port;
|
||||||
|
if (parsedUrl.m_Scheme == "http") {
|
||||||
|
if (!parsedUrl.GetPort(&port)) {
|
||||||
|
port = HTTP_PORT;
|
||||||
|
}
|
||||||
|
cli =
|
||||||
|
std::make_unique<httplib::Client>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS);
|
||||||
|
} else if (parsedUrl.m_Scheme == "https") {
|
||||||
|
if (!parsedUrl.GetPort(&port)) {
|
||||||
|
port = HTTPS_PORT;
|
||||||
|
}
|
||||||
|
cli = std::make_unique<httplib::SSLClient>(parsedUrl.m_Host.c_str(), port,
|
||||||
|
TIMEOUT_SECONDS);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(WebService, "Bad URL scheme {}", parsedUrl.m_Scheme);
|
||||||
|
return Common::WebResult{Common::WebResult::Code::InvalidURL, "Bad URL scheme"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cli == nullptr) {
|
||||||
|
LOG_ERROR(WebService, "Invalid URL {}", host + path);
|
||||||
|
return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"};
|
||||||
}
|
}
|
||||||
|
|
||||||
hl::Request request;
|
httplib::Headers params;
|
||||||
request.method = "POST";
|
if (!jwt.empty()) {
|
||||||
request.path = "/" + parsed_url.m_Path;
|
params = {
|
||||||
|
{std::string("Authorization"), fmt::format("Bearer {}", jwt)},
|
||||||
|
};
|
||||||
|
} else if (!username.empty()) {
|
||||||
|
params = {
|
||||||
|
{std::string("x-username"), username},
|
||||||
|
{std::string("x-token"), token},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
params.emplace(std::string("api-version"), std::string(API_VERSION));
|
||||||
|
if (method != "GET") {
|
||||||
|
params.emplace(std::string("Content-Type"), std::string("application/json"));
|
||||||
|
};
|
||||||
|
|
||||||
|
httplib::Request request;
|
||||||
|
request.method = method;
|
||||||
|
request.path = path;
|
||||||
request.headers = params;
|
request.headers = params;
|
||||||
request.body = data;
|
request.body = data;
|
||||||
|
|
||||||
hl::Response response;
|
httplib::Response response;
|
||||||
|
|
||||||
if (!cli->send(request, response)) {
|
if (!cli->send(request, response)) {
|
||||||
LOG_ERROR(WebService, "POST to {} returned null", url);
|
LOG_ERROR(WebService, "{} to {} returned null", method, host + path);
|
||||||
return Common::WebResult{Common::WebResult::Code::LibError, "Null response"};
|
return Common::WebResult{Common::WebResult::Code::LibError, "Null response"};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status >= 400) {
|
if (response.status >= 400) {
|
||||||
LOG_ERROR(WebService, "POST to {} returned error status code: {}", url, response.status);
|
LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path,
|
||||||
if (response.status == 401 && !is_jwt_requested && is_first_attempt) {
|
response.status);
|
||||||
LOG_WARNING(WebService, "Requesting new JWT");
|
|
||||||
UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token);
|
|
||||||
is_first_attempt = false;
|
|
||||||
PostJsonAsyncFn(url, parsed_url, params, data, is_jwt_requested);
|
|
||||||
is_first_attempt = true;
|
|
||||||
}
|
|
||||||
return Common::WebResult{Common::WebResult::Code::HttpError,
|
return Common::WebResult{Common::WebResult::Code::HttpError,
|
||||||
std::to_string(response.status)};
|
std::to_string(response.status)};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto content_type = response.headers.find("content-type");
|
auto content_type = response.headers.find("content-type");
|
||||||
|
|
||||||
if (content_type == response.headers.end() ||
|
if (content_type == response.headers.end()) {
|
||||||
(content_type->second.find("application/json") == std::string::npos &&
|
LOG_ERROR(WebService, "{} to {} returned no content", method, host + path);
|
||||||
content_type->second.find("text/html; charset=utf-8") == std::string::npos)) {
|
|
||||||
LOG_ERROR(WebService, "POST to {} returned wrong content: {}", url, content_type->second);
|
|
||||||
return Common::WebResult{Common::WebResult::Code::WrongContent, ""};
|
return Common::WebResult{Common::WebResult::Code::WrongContent, ""};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (content_type->second.find("application/json") == std::string::npos &&
|
||||||
|
content_type->second.find("text/html; charset=utf-8") == std::string::npos) {
|
||||||
|
LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path,
|
||||||
|
content_type->second);
|
||||||
|
return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"};
|
||||||
|
}
|
||||||
return Common::WebResult{Common::WebResult::Code::Success, "", response.body};
|
return Common::WebResult{Common::WebResult::Code::Success, "", response.body};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::future<Common::WebResult> PostJson(const std::string& url, const std::string& data,
|
void Client::UpdateJWT() {
|
||||||
bool allow_anonymous) {
|
if (!username.empty() && !token.empty()) {
|
||||||
|
auto result = GenericJson("POST", "/jwt/internal", "", "", username, token);
|
||||||
using lup = LUrlParser::clParseURL;
|
if (result.result_code != Common::WebResult::Code::Success) {
|
||||||
namespace hl = httplib;
|
LOG_ERROR(WebService, "UpdateJWT failed");
|
||||||
|
} else {
|
||||||
lup parsedUrl = lup::ParseURL(url);
|
std::lock_guard<std::mutex> lock(jwt_cache.mutex);
|
||||||
|
jwt_cache.username = username;
|
||||||
if (url.empty() || !parsedUrl.IsValid()) {
|
jwt_cache.token = token;
|
||||||
LOG_ERROR(WebService, "URL is invalid");
|
jwt_cache.jwt = jwt = result.returned_data;
|
||||||
return std::async(std::launch::deferred, [] {
|
}
|
||||||
return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string jwt =
|
|
||||||
UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token);
|
|
||||||
|
|
||||||
const bool are_credentials_provided{!jwt.empty()};
|
|
||||||
if (!allow_anonymous && !are_credentials_provided) {
|
|
||||||
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
|
|
||||||
return std::async(std::launch::deferred, [] {
|
|
||||||
return Common::WebResult{Common::WebResult::Code::CredentialsMissing,
|
|
||||||
"Credentials needed"};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Built request header
|
|
||||||
hl::Headers params;
|
|
||||||
if (are_credentials_provided) {
|
|
||||||
// Authenticated request if credentials are provided
|
|
||||||
params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)},
|
|
||||||
{std::string("api-version"), std::string(API_VERSION)},
|
|
||||||
{std::string("Content-Type"), std::string("application/json")}};
|
|
||||||
} else {
|
|
||||||
// Otherwise, anonymous request
|
|
||||||
params = {{std::string("api-version"), std::string(API_VERSION)},
|
|
||||||
{std::string("Content-Type"), std::string("application/json")}};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post JSON asynchronously
|
|
||||||
return std::async(std::launch::async, PostJsonAsyncFn, url, parsedUrl, params, data, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::future<Common::WebResult> PostJson(const std::string& url, const std::string& username,
|
Common::WebResult Client::GenericJson(const std::string& method, const std::string& path,
|
||||||
const std::string& token) {
|
const std::string& data, bool allow_anonymous) {
|
||||||
using lup = LUrlParser::clParseURL;
|
if (jwt.empty()) {
|
||||||
namespace hl = httplib;
|
UpdateJWT();
|
||||||
|
|
||||||
lup parsedUrl = lup::ParseURL(url);
|
|
||||||
|
|
||||||
if (url.empty() || !parsedUrl.IsValid()) {
|
|
||||||
LOG_ERROR(WebService, "URL is invalid");
|
|
||||||
return std::async(std::launch::deferred, [] {
|
|
||||||
return Common::WebResult{Common::WebResult::Code::InvalidURL, ""};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool are_credentials_provided{!token.empty() && !username.empty()};
|
if (jwt.empty() && !allow_anonymous) {
|
||||||
if (!are_credentials_provided) {
|
|
||||||
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
|
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
|
||||||
return std::async(std::launch::deferred, [] {
|
return Common::WebResult{Common::WebResult::Code::CredentialsMissing, "Credentials needed"};
|
||||||
return Common::WebResult{Common::WebResult::Code::CredentialsMissing, ""};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Built request header
|
auto result = GenericJson(method, path, data, jwt);
|
||||||
hl::Headers params;
|
if (result.result_string == "401") {
|
||||||
if (are_credentials_provided) {
|
// Try again with new JWT
|
||||||
// Authenticated request if credentials are provided
|
UpdateJWT();
|
||||||
params = {{std::string("x-username"), username},
|
result = GenericJson(method, path, data, jwt);
|
||||||
{std::string("x-token"), token},
|
|
||||||
{std::string("api-version"), std::string(API_VERSION)},
|
|
||||||
{std::string("Content-Type"), std::string("application/json")}};
|
|
||||||
} else {
|
|
||||||
// Otherwise, anonymous request
|
|
||||||
params = {{std::string("api-version"), std::string(API_VERSION)},
|
|
||||||
{std::string("Content-Type"), std::string("application/json")}};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post JSON asynchronously
|
return result;
|
||||||
return std::async(std::launch::async, PostJsonAsyncFn, url, parsedUrl, params, "", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
std::future<T> GetJson(std::function<T(const std::string&)> func, const std::string& url,
|
|
||||||
bool allow_anonymous) {
|
|
||||||
static bool is_first_attempt = true;
|
|
||||||
|
|
||||||
using lup = LUrlParser::clParseURL;
|
|
||||||
namespace hl = httplib;
|
|
||||||
|
|
||||||
lup parsedUrl = lup::ParseURL(url);
|
|
||||||
|
|
||||||
if (url.empty() || !parsedUrl.IsValid()) {
|
|
||||||
LOG_ERROR(WebService, "URL is invalid");
|
|
||||||
return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); });
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string jwt =
|
|
||||||
UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token);
|
|
||||||
|
|
||||||
const bool are_credentials_provided{!jwt.empty()};
|
|
||||||
if (!allow_anonymous && !are_credentials_provided) {
|
|
||||||
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
|
|
||||||
return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Built request header
|
|
||||||
hl::Headers params;
|
|
||||||
if (are_credentials_provided) {
|
|
||||||
params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)},
|
|
||||||
{std::string("api-version"), std::string(API_VERSION)}};
|
|
||||||
} else {
|
|
||||||
// Otherwise, anonymous request
|
|
||||||
params = {{std::string("api-version"), std::string(API_VERSION)}};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get JSON asynchronously
|
|
||||||
return std::async(std::launch::async, [func, url, parsedUrl, params, allow_anonymous] {
|
|
||||||
std::unique_ptr<hl::Client> cli = GetClientFor(parsedUrl);
|
|
||||||
|
|
||||||
if (cli == nullptr) {
|
|
||||||
return func("");
|
|
||||||
}
|
|
||||||
|
|
||||||
hl::Request request;
|
|
||||||
request.method = "GET";
|
|
||||||
request.path = "/" + parsedUrl.m_Path;
|
|
||||||
request.headers = params;
|
|
||||||
|
|
||||||
hl::Response response;
|
|
||||||
|
|
||||||
if (!cli->send(request, response)) {
|
|
||||||
LOG_ERROR(WebService, "GET to {} returned null", url);
|
|
||||||
return func("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status >= 400) {
|
|
||||||
LOG_ERROR(WebService, "GET to {} returned error status code: {}", url, response.status);
|
|
||||||
if (response.status == 401 && is_first_attempt) {
|
|
||||||
LOG_WARNING(WebService, "Requesting new JWT");
|
|
||||||
UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token);
|
|
||||||
is_first_attempt = false;
|
|
||||||
GetJson(func, url, allow_anonymous);
|
|
||||||
is_first_attempt = true;
|
|
||||||
}
|
|
||||||
return func("");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto content_type = response.headers.find("content-type");
|
|
||||||
|
|
||||||
if (content_type == response.headers.end() ||
|
|
||||||
content_type->second.find("application/json") == std::string::npos) {
|
|
||||||
LOG_ERROR(WebService, "GET to {} returned wrong content: {}", url,
|
|
||||||
content_type->second);
|
|
||||||
return func("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(response.body);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
template std::future<bool> GetJson(std::function<bool(const std::string&)> func,
|
|
||||||
const std::string& url, bool allow_anonymous);
|
|
||||||
template std::future<AnnounceMultiplayerRoom::RoomList> GetJson(
|
|
||||||
std::function<AnnounceMultiplayerRoom::RoomList(const std::string&)> func,
|
|
||||||
const std::string& url, bool allow_anonymous);
|
|
||||||
|
|
||||||
void DeleteJson(const std::string& url, const std::string& data) {
|
|
||||||
static bool is_first_attempt = true;
|
|
||||||
|
|
||||||
using lup = LUrlParser::clParseURL;
|
|
||||||
namespace hl = httplib;
|
|
||||||
|
|
||||||
lup parsedUrl = lup::ParseURL(url);
|
|
||||||
|
|
||||||
if (url.empty() || !parsedUrl.IsValid()) {
|
|
||||||
LOG_ERROR(WebService, "URL is invalid");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string jwt =
|
|
||||||
UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token);
|
|
||||||
|
|
||||||
const bool are_credentials_provided{!jwt.empty()};
|
|
||||||
if (!are_credentials_provided) {
|
|
||||||
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Built request header
|
|
||||||
hl::Headers params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)},
|
|
||||||
{std::string("api-version"), std::string(API_VERSION)},
|
|
||||||
{std::string("Content-Type"), std::string("application/json")}};
|
|
||||||
|
|
||||||
// Delete JSON asynchronously
|
|
||||||
std::async(std::launch::async, [url, parsedUrl, params, data] {
|
|
||||||
std::unique_ptr<hl::Client> cli = GetClientFor(parsedUrl);
|
|
||||||
|
|
||||||
if (cli == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hl::Request request;
|
|
||||||
request.method = "DELETE";
|
|
||||||
request.path = "/" + parsedUrl.m_Path;
|
|
||||||
request.headers = params;
|
|
||||||
request.body = data;
|
|
||||||
|
|
||||||
hl::Response response;
|
|
||||||
|
|
||||||
if (!cli->send(request, response)) {
|
|
||||||
LOG_ERROR(WebService, "DELETE to {} returned null", url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status >= 400) {
|
|
||||||
LOG_ERROR(WebService, "DELETE to {} returned error status code: {}", url,
|
|
||||||
response.status);
|
|
||||||
if (response.status == 401 && is_first_attempt) {
|
|
||||||
LOG_WARNING(WebService, "Requesting new JWT");
|
|
||||||
UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token);
|
|
||||||
is_first_attempt = false;
|
|
||||||
DeleteJson(url, data);
|
|
||||||
is_first_attempt = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto content_type = response.headers.find("content-type");
|
|
||||||
|
|
||||||
if (content_type == response.headers.end() ||
|
|
||||||
content_type->second.find("application/json") == std::string::npos) {
|
|
||||||
LOG_ERROR(WebService, "DELETE to {} returned wrong content: {}", url,
|
|
||||||
content_type->second);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace WebService
|
} // namespace WebService
|
||||||
|
|
|
@ -5,79 +5,88 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <future>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <httplib.h>
|
#include <httplib.h>
|
||||||
#include "common/announce_multiplayer_room.h"
|
#include "common/announce_multiplayer_room.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
|
||||||
namespace LUrlParser {
|
namespace httplib {
|
||||||
class clParseURL;
|
class Client;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace WebService {
|
namespace WebService {
|
||||||
|
|
||||||
/**
|
class Client {
|
||||||
* Requests a new JWT if necessary
|
public:
|
||||||
* @param force_new_token If true, force to request a new token from the server.
|
Client(const std::string& host, const std::string& username, const std::string& token);
|
||||||
* @param username Citra username to use for authentication.
|
|
||||||
* @param token Citra token to use for authentication.
|
|
||||||
* @return string with the current JWT toke
|
|
||||||
*/
|
|
||||||
std::string UpdateCoreJWT(bool force_new_token, const std::string& username,
|
|
||||||
const std::string& token);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Posts JSON to a api.citra-emu.org.
|
* Posts JSON to the specified path.
|
||||||
* @param url URL of the api.citra-emu.org endpoint to post data to.
|
* @param path the URL segment after the host address.
|
||||||
* @param parsed_url Parsed URL used for the POST request.
|
* @param data String of JSON data to use for the body of the POST request.
|
||||||
* @param params Headers sent for the POST request.
|
* @param allow_anonymous If true, allow anonymous unauthenticated requests.
|
||||||
* @param data String of JSON data to use for the body of the POST request.
|
* @return the result of the request.
|
||||||
* @param data If true, a JWT is requested in the function
|
*/
|
||||||
* @return future with the returned value of the POST
|
Common::WebResult PostJson(const std::string& path, const std::string& data,
|
||||||
*/
|
bool allow_anonymous) {
|
||||||
static Common::WebResult PostJsonAsyncFn(const std::string& url,
|
return GenericJson("POST", path, data, allow_anonymous);
|
||||||
const LUrlParser::clParseURL& parsed_url,
|
}
|
||||||
const httplib::Headers& params, const std::string& data,
|
|
||||||
bool is_jwt_requested);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Posts JSON to api.citra-emu.org.
|
* Gets JSON from the specified path.
|
||||||
* @param url URL of the api.citra-emu.org endpoint to post data to.
|
* @param path the URL segment after the host address.
|
||||||
* @param data String of JSON data to use for the body of the POST request.
|
* @param allow_anonymous If true, allow anonymous unauthenticated requests.
|
||||||
* @param allow_anonymous If true, allow anonymous unauthenticated requests.
|
* @return the result of the request.
|
||||||
* @return future with the returned value of the POST
|
*/
|
||||||
*/
|
Common::WebResult GetJson(const std::string& path, bool allow_anonymous) {
|
||||||
std::future<Common::WebResult> PostJson(const std::string& url, const std::string& data,
|
return GenericJson("GET", path, "", allow_anonymous);
|
||||||
bool allow_anonymous);
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Posts JSON to api.citra-emu.org.
|
* Deletes JSON to the specified path.
|
||||||
* @param url URL of the api.citra-emu.org endpoint to post data to.
|
* @param path the URL segment after the host address.
|
||||||
* @param username Citra username to use for authentication.
|
* @param data String of JSON data to use for the body of the DELETE request.
|
||||||
* @param token Citra token to use for authentication.
|
* @param allow_anonymous If true, allow anonymous unauthenticated requests.
|
||||||
* @return future with the error or result of the POST
|
* @return the result of the request.
|
||||||
*/
|
*/
|
||||||
std::future<Common::WebResult> PostJson(const std::string& url, const std::string& username,
|
Common::WebResult DeleteJson(const std::string& path, const std::string& data,
|
||||||
const std::string& token);
|
bool allow_anonymous) {
|
||||||
|
return GenericJson("DELETE", path, data, allow_anonymous);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
private:
|
||||||
* Gets JSON from api.citra-emu.org.
|
/// A generic function handles POST, GET and DELETE request together
|
||||||
* @param func A function that gets exectued when the json as a string is received
|
Common::WebResult GenericJson(const std::string& method, const std::string& path,
|
||||||
* @param url URL of the api.citra-emu.org endpoint to post data to.
|
const std::string& data, bool allow_anonymous);
|
||||||
* @param allow_anonymous If true, allow anonymous unauthenticated requests.
|
|
||||||
* @return future that holds the return value T of the func
|
|
||||||
*/
|
|
||||||
template <typename T>
|
|
||||||
std::future<T> GetJson(std::function<T(const std::string&)> func, const std::string& url,
|
|
||||||
bool allow_anonymous);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete JSON to api.citra-emu.org.
|
* A generic function with explicit authentication method specified
|
||||||
* @param url URL of the api.citra-emu.org endpoint to post data to.
|
* JWT is used if the jwt parameter is not empty
|
||||||
* @param data String of JSON data to use for the body of the DELETE request.
|
* username + token is used if jwt is empty but username and token are not empty
|
||||||
*/
|
* anonymous if all of jwt, username and token are empty
|
||||||
void DeleteJson(const std::string& url, const std::string& data);
|
*/
|
||||||
|
Common::WebResult GenericJson(const std::string& method, const std::string& path,
|
||||||
|
const std::string& data, const std::string& jwt = "",
|
||||||
|
const std::string& username = "", const std::string& token = "");
|
||||||
|
|
||||||
|
// Retrieve a new JWT from given username and token
|
||||||
|
void UpdateJWT();
|
||||||
|
|
||||||
|
std::string host;
|
||||||
|
std::string username;
|
||||||
|
std::string token;
|
||||||
|
std::string jwt;
|
||||||
|
std::unique_ptr<httplib::Client> cli;
|
||||||
|
|
||||||
|
struct JWTCache {
|
||||||
|
std::mutex mutex;
|
||||||
|
std::string username;
|
||||||
|
std::string token;
|
||||||
|
std::string jwt;
|
||||||
|
};
|
||||||
|
static JWTCache jwt_cache;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace WebService
|
} // namespace WebService
|
||||||
|
|
Loading…
Reference in a new issue