mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	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/emu_window/emu_window_sdl2.h" | ||||
| #include "common/common_paths.h" | ||||
| #include "common/detached_tasks.h" | ||||
| #include "common/file_util.h" | ||||
| #include "common/logging/backend.h" | ||||
| #include "common/logging/filter.h" | ||||
|  | @ -129,6 +130,7 @@ static void InitializeLogging() { | |||
| 
 | ||||
| /// Application entry point
 | ||||
| int main(int argc, char** argv) { | ||||
|     Common::DetachedTasks detached_tasks; | ||||
|     Config config; | ||||
|     int option_index = 0; | ||||
|     bool use_gdbstub = Settings::values.use_gdbstub; | ||||
|  | @ -344,5 +346,6 @@ int main(int argc, char** argv) { | |||
| 
 | ||||
|     Core::Movie::GetInstance().Shutdown(); | ||||
| 
 | ||||
|     detached_tasks.WaitForAllTasks(); | ||||
|     return 0; | ||||
| } | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
| 
 | ||||
| #include <QIcon> | ||||
| #include <QMessageBox> | ||||
| #include <QtConcurrent/QtConcurrentRun> | ||||
| #include "citra_qt/configuration/configure_web.h" | ||||
| #include "citra_qt/ui_settings.h" | ||||
| #include "core/settings.h" | ||||
|  | @ -16,7 +17,7 @@ ConfigureWeb::ConfigureWeb(QWidget* parent) | |||
|     connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, this, | ||||
|             &ConfigureWeb::RefreshTelemetryID); | ||||
|     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 | ||||
|     ui->discord_group->setVisible(false); | ||||
|  | @ -89,17 +90,19 @@ void ConfigureWeb::OnLoginChanged() { | |||
| } | ||||
| 
 | ||||
| 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->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() { | ||||
|     ui->button_verify_login->setEnabled(true); | ||||
|     ui->button_verify_login->setText(tr("Verify")); | ||||
|     if (verified.get()) { | ||||
|     if (verify_watcher.result()) { | ||||
|         user_verified = true; | ||||
|         ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||||
|         ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||||
|  |  | |||
|  | @ -4,8 +4,8 @@ | |||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <future> | ||||
| #include <memory> | ||||
| #include <QFutureWatcher> | ||||
| #include <QWidget> | ||||
| 
 | ||||
| namespace Ui { | ||||
|  | @ -28,14 +28,11 @@ public slots: | |||
|     void VerifyLogin(); | ||||
|     void OnLoginVerified(); | ||||
| 
 | ||||
| signals: | ||||
|     void LoginVerified(); | ||||
| 
 | ||||
| private: | ||||
|     void setConfiguration(); | ||||
| 
 | ||||
|     bool user_verified = true; | ||||
|     std::future<bool> verified; | ||||
|     QFutureWatcher<bool> verify_watcher; | ||||
| 
 | ||||
|     std::unique_ptr<Ui::ConfigureWeb> ui; | ||||
| }; | ||||
|  |  | |||
|  | @ -43,6 +43,7 @@ | |||
| #include "citra_qt/updater/updater.h" | ||||
| #include "citra_qt/util/clickable_label.h" | ||||
| #include "common/common_paths.h" | ||||
| #include "common/detached_tasks.h" | ||||
| #include "common/logging/backend.h" | ||||
| #include "common/logging/filter.h" | ||||
| #include "common/logging/log.h" | ||||
|  | @ -1666,6 +1667,7 @@ void GMainWindow::SetDiscordEnabled(bool state) { | |||
| #endif | ||||
| 
 | ||||
| int main(int argc, char* argv[]) { | ||||
|     Common::DetachedTasks detached_tasks; | ||||
|     MicroProfileOnThreadCreate("Frontend"); | ||||
|     SCOPE_EXIT({ MicroProfileShutdown(); }); | ||||
| 
 | ||||
|  | @ -1691,5 +1693,7 @@ int main(int argc, char* argv[]) { | |||
|     Frontend::RegisterSoftwareKeyboard(std::make_shared<QtKeyboard>(main_window)); | ||||
| 
 | ||||
|     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); | ||||
| 
 | ||||
|     // 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
 | ||||
|     // 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() { | ||||
|     if (auto session = announce_multiplayer_session.lock()) { | ||||
|         ResetModel(); | ||||
|         room_list_future = session->GetRoomList([&]() { emit LobbyRefreshed(); }); | ||||
|         ui->refresh_list->setEnabled(false); | ||||
|         ui->refresh_list->setText(tr("Refreshing")); | ||||
|         room_list_watcher.setFuture( | ||||
|             QtConcurrent::run([session]() { return session->GetRoomList(); })); | ||||
|     } else { | ||||
|         // TODO(jroweboy): Display an error box about announce couldn't be started
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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) { | ||||
|         // find the icon for the game if this person owns that game.
 | ||||
|         QPixmap smdh_icon; | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ | |||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <future> | ||||
| #include <memory> | ||||
| #include <QDialog> | ||||
| #include <QFutureWatcher> | ||||
|  | @ -61,11 +60,6 @@ private slots: | |||
|     void OnJoinRoom(const QModelIndex&); | ||||
| 
 | ||||
| signals: | ||||
|     /**
 | ||||
|      * Signalled when the latest lobby data is retrieved. | ||||
|      */ | ||||
|     void LobbyRefreshed(); | ||||
| 
 | ||||
|     void StateChanged(const Network::RoomMember::State&); | ||||
| 
 | ||||
| private: | ||||
|  | @ -84,7 +78,7 @@ private: | |||
|     QStandardItemModel* game_list; | ||||
|     LobbyFilterProxyModel* proxy; | ||||
| 
 | ||||
|     std::future<AnnounceMultiplayerRoom::RoomList> room_list_future; | ||||
|     QFutureWatcher<AnnounceMultiplayerRoom::RoomList> room_list_watcher; | ||||
|     std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; | ||||
|     std::unique_ptr<Ui::Lobby> ui; | ||||
|     QFutureWatcher<void>* watcher; | ||||
|  |  | |||
|  | @ -42,6 +42,8 @@ add_library(common STATIC | |||
|     alignment.h | ||||
|     announce_multiplayer_room.h | ||||
|     assert.h | ||||
|     detached_tasks.cpp | ||||
|     detached_tasks.h | ||||
|     bit_field.h | ||||
|     bit_set.h | ||||
|     chunk_file.h | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ | |||
| 
 | ||||
| #include <array> | ||||
| #include <functional> | ||||
| #include <future> | ||||
| #include <string> | ||||
| #include <vector> | ||||
| #include "common/common_types.h" | ||||
|  | @ -90,7 +89,7 @@ public: | |||
|      * Send the data to the announce service | ||||
|      * @result The result of the announce attempt | ||||
|      */ | ||||
|     virtual std::future<Common::WebResult> Announce() = 0; | ||||
|     virtual Common::WebResult Announce() = 0; | ||||
| 
 | ||||
|     /**
 | ||||
|      * Empties the stored players | ||||
|  | @ -99,11 +98,9 @@ public: | |||
| 
 | ||||
|     /**
 | ||||
|      * 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 | ||||
|      */ | ||||
|     virtual std::future<RoomList> GetRoomList(std::function<void()> func) = 0; | ||||
|     virtual RoomList GetRoomList() = 0; | ||||
| 
 | ||||
|     /**
 | ||||
|      * Sends a delete message to the announce service | ||||
|  | @ -124,18 +121,12 @@ public: | |||
|                             const u64 /*preferred_game_id*/) override {} | ||||
|     void AddPlayer(const std::string& /*nickname*/, const MacAddress& /*mac_address*/, | ||||
|                    const u64 /*game_id*/, const std::string& /*game_name*/) override {} | ||||
|     std::future<Common::WebResult> Announce() override { | ||||
|         return std::async(std::launch::deferred, []() { | ||||
|             return Common::WebResult{Common::WebResult::Code::NoWebservice, | ||||
|                                      "WebService is missing"}; | ||||
|         }); | ||||
|     Common::WebResult Announce() override { | ||||
|         return Common::WebResult{Common::WebResult::Code::NoWebservice, "WebService is missing"}; | ||||
|     } | ||||
|     void ClearPlayers() override {} | ||||
|     std::future<RoomList> GetRoomList(std::function<void()> func) override { | ||||
|         return std::async(std::launch::deferred, [func]() { | ||||
|             func(); | ||||
|             return RoomList{}; | ||||
|         }); | ||||
|     RoomList GetRoomList() override { | ||||
|         return RoomList{}; | ||||
|     } | ||||
| 
 | ||||
|     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() { | ||||
| #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_token); | ||||
| #else | ||||
|  | @ -87,22 +87,18 @@ void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { | |||
|             backend->AddPlayer(member.nickname, member.mac_address, member.game_info.id, | ||||
|                                member.game_info.name); | ||||
|         } | ||||
|         future = backend->Announce(); | ||||
|         if (future.valid()) { | ||||
|             Common::WebResult result = future.get(); | ||||
|             if (result.result_code != Common::WebResult::Code::Success) { | ||||
|                 std::lock_guard<std::mutex> lock(callback_mutex); | ||||
|                 for (auto callback : error_callbacks) { | ||||
|                     (*callback)(result); | ||||
|                 } | ||||
|         Common::WebResult result = backend->Announce(); | ||||
|         if (result.result_code != Common::WebResult::Code::Success) { | ||||
|             std::lock_guard<std::mutex> lock(callback_mutex); | ||||
|             for (auto callback : error_callbacks) { | ||||
|                 (*callback)(result); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| std::future<AnnounceMultiplayerRoom::RoomList> AnnounceMultiplayerSession::GetRoomList( | ||||
|     std::function<void()> func) { | ||||
|     return backend->GetRoomList(func); | ||||
| AnnounceMultiplayerRoom::RoomList AnnounceMultiplayerSession::GetRoomList() { | ||||
|     return backend->GetRoomList(); | ||||
| } | ||||
| 
 | ||||
| } // namespace Core
 | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ public: | |||
|      * @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 | ||||
|      */ | ||||
|     std::future<AnnounceMultiplayerRoom::RoomList> GetRoomList(std::function<void()> func); | ||||
|     AnnounceMultiplayerRoom::RoomList GetRoomList(); | ||||
| 
 | ||||
| private: | ||||
|     Common::Event shutdown_event; | ||||
|  |  | |||
|  | @ -82,24 +82,20 @@ u64 RegenerateTelemetryId() { | |||
|     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 | ||||
|     return WebService::VerifyLogin(username, token, Settings::values.web_api_url + "/profile", | ||||
|                                    func); | ||||
|     return WebService::VerifyLogin(Settings::values.web_api_url, username, token); | ||||
| #else | ||||
|     return std::async(std::launch::async, [func{std::move(func)}]() { | ||||
|         func(); | ||||
|         return false; | ||||
|     }); | ||||
|     return false; | ||||
| #endif | ||||
| } | ||||
| 
 | ||||
| TelemetrySession::TelemetrySession() { | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
|     if (Settings::values.enable_telemetry) { | ||||
|         backend = std::make_unique<WebService::TelemetryJson>( | ||||
|             Settings::values.web_api_url + "/telemetry", Settings::values.citra_username, | ||||
|             Settings::values.citra_token); | ||||
|         backend = std::make_unique<WebService::TelemetryJson>(Settings::values.web_api_url, | ||||
|                                                               Settings::values.citra_username, | ||||
|                                                               Settings::values.citra_token); | ||||
|     } else { | ||||
|         backend = std::make_unique<Telemetry::NullVisitor>(); | ||||
|     } | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ | |||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <future> | ||||
| #include <memory> | ||||
| #include "common/telemetry.h" | ||||
| 
 | ||||
|  | @ -31,6 +30,8 @@ public: | |||
|         field_collection.AddField(type, name, std::move(value)); | ||||
|     } | ||||
| 
 | ||||
|     static void FinalizeAsyncJob(); | ||||
| 
 | ||||
| private: | ||||
|     Telemetry::FieldCollection field_collection; ///< Tracks all added fields for the session
 | ||||
|     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 | ||||
|  * @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
 | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ | |||
| #endif | ||||
| 
 | ||||
| #include "common/common_types.h" | ||||
| #include "common/detached_tasks.h" | ||||
| #include "common/scm_rev.h" | ||||
| #include "core/announce_multiplayer_session.h" | ||||
| #include "core/core.h" | ||||
|  | @ -54,6 +55,7 @@ static void PrintVersion() { | |||
| 
 | ||||
| /// Application entry point
 | ||||
| int main(int argc, char** argv) { | ||||
|     Common::DetachedTasks detached_tasks; | ||||
|     int option_index = 0; | ||||
|     char* endarg; | ||||
| 
 | ||||
|  | @ -204,5 +206,6 @@ int main(int argc, char** argv) { | |||
|         room->Destroy(); | ||||
|     } | ||||
|     Network::Shutdown(); | ||||
|     detached_tasks.WaitForAllTasks(); | ||||
|     return 0; | ||||
| } | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <future> | ||||
| #include "common/detached_tasks.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "web_service/announce_room_json.h" | ||||
| #include "web_service/json.h" | ||||
|  | @ -82,30 +83,31 @@ void RoomJson::AddPlayer(const std::string& nickname, | |||
|     room.members.push_back(member); | ||||
| } | ||||
| 
 | ||||
| std::future<Common::WebResult> RoomJson::Announce() { | ||||
| Common::WebResult RoomJson::Announce() { | ||||
|     nlohmann::json json = room; | ||||
|     return PostJson(endpoint_url, json.dump(), false); | ||||
|     return client.PostJson("/lobby", json.dump(), false); | ||||
| } | ||||
| 
 | ||||
| void RoomJson::ClearPlayers() { | ||||
|     room.members.clear(); | ||||
| } | ||||
| 
 | ||||
| std::future<AnnounceMultiplayerRoom::RoomList> RoomJson::GetRoomList(std::function<void()> func) { | ||||
|     auto DeSerialize = [func](const std::string& reply) -> AnnounceMultiplayerRoom::RoomList { | ||||
|         nlohmann::json json = nlohmann::json::parse(reply); | ||||
|         AnnounceMultiplayerRoom::RoomList room_list = | ||||
|             json.at("rooms").get<AnnounceMultiplayerRoom::RoomList>(); | ||||
|         func(); | ||||
|         return room_list; | ||||
|     }; | ||||
|     return GetJson<AnnounceMultiplayerRoom::RoomList>(DeSerialize, endpoint_url, true); | ||||
| AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() { | ||||
|     auto reply = client.GetJson("/lobby", true).returned_data; | ||||
|     if (reply.empty()) { | ||||
|         return {}; | ||||
|     } | ||||
|     return nlohmann::json::parse(reply).at("rooms").get<AnnounceMultiplayerRoom::RoomList>(); | ||||
| } | ||||
| 
 | ||||
| void RoomJson::Delete() { | ||||
|     nlohmann::json json; | ||||
|     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
 | ||||
|  |  | |||
|  | @ -5,9 +5,9 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <functional> | ||||
| #include <future> | ||||
| #include <string> | ||||
| #include "common/announce_multiplayer_room.h" | ||||
| #include "web_service/web_backend.h" | ||||
| 
 | ||||
| namespace WebService { | ||||
| 
 | ||||
|  | @ -17,8 +17,8 @@ namespace WebService { | |||
|  */ | ||||
| class RoomJson : public AnnounceMultiplayerRoom::Backend { | ||||
| public: | ||||
|     RoomJson(const std::string& endpoint_url, const std::string& username, const std::string& token) | ||||
|         : endpoint_url(endpoint_url), username(username), token(token) {} | ||||
|     RoomJson(const std::string& host, const std::string& username, const std::string& token) | ||||
|         : client(host, username, token), host(host), username(username), token(token) {} | ||||
|     ~RoomJson() = default; | ||||
|     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, | ||||
|  | @ -27,14 +27,15 @@ public: | |||
|     void AddPlayer(const std::string& nickname, | ||||
|                    const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, | ||||
|                    const std::string& game_name) override; | ||||
|     std::future<Common::WebResult> Announce() override; | ||||
|     Common::WebResult Announce() override; | ||||
|     void ClearPlayers() override; | ||||
|     std::future<AnnounceMultiplayerRoom::RoomList> GetRoomList(std::function<void()> func) override; | ||||
|     AnnounceMultiplayerRoom::RoomList GetRoomList() override; | ||||
|     void Delete() override; | ||||
| 
 | ||||
| private: | ||||
|     AnnounceMultiplayerRoom::Room room; | ||||
|     std::string endpoint_url; | ||||
|     Client client; | ||||
|     std::string host; | ||||
|     std::string username; | ||||
|     std::string token; | ||||
| }; | ||||
|  |  | |||
|  | @ -2,7 +2,9 @@ | |||
| // 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" | ||||
| #include "web_service/telemetry_json.h" | ||||
| #include "web_service/web_backend.h" | ||||
| 
 | ||||
|  | @ -81,8 +83,12 @@ void TelemetryJson::Complete() { | |||
|     SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig"); | ||||
|     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
 | ||||
|     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
 | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <array> | ||||
| #include <future> | ||||
| #include <string> | ||||
| #include "common/announce_multiplayer_room.h" | ||||
| #include "common/telemetry.h" | ||||
|  | @ -19,9 +18,8 @@ namespace WebService { | |||
|  */ | ||||
| class TelemetryJson : public Telemetry::VisitorInterface { | ||||
| public: | ||||
|     TelemetryJson(const std::string& endpoint_url, const std::string& username, | ||||
|                   const std::string& token) | ||||
|         : endpoint_url(endpoint_url), username(username), token(token) {} | ||||
|     TelemetryJson(const std::string& host, const std::string& username, const std::string& token) | ||||
|         : host(host), username(username), token(token) {} | ||||
|     ~TelemetryJson() = default; | ||||
| 
 | ||||
|     void Visit(const Telemetry::Field<bool>& field) override; | ||||
|  | @ -53,10 +51,9 @@ private: | |||
| 
 | ||||
|     nlohmann::json output; | ||||
|     std::array<nlohmann::json, 7> sections; | ||||
|     std::string endpoint_url; | ||||
|     std::string host; | ||||
|     std::string username; | ||||
|     std::string token; | ||||
|     std::future<Common::WebResult> future; | ||||
| }; | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
|  | @ -8,26 +8,20 @@ | |||
| 
 | ||||
| namespace WebService { | ||||
| 
 | ||||
| std::future<bool> VerifyLogin(std::string& username, std::string& token, | ||||
|                               const std::string& endpoint_url, std::function<void()> func) { | ||||
|     auto get_func = [func, username](const std::string& reply) -> bool { | ||||
|         func(); | ||||
| bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token) { | ||||
|     Client client(host, username, token); | ||||
|     auto reply = client.GetJson("/profile", false).returned_data; | ||||
|     if (reply.empty()) { | ||||
|         return false; | ||||
|     } | ||||
|     nlohmann::json json = nlohmann::json::parse(reply); | ||||
|     const auto iter = json.find("username"); | ||||
| 
 | ||||
|         if (reply.empty()) { | ||||
|             return false; | ||||
|         } | ||||
|     if (iter == json.end()) { | ||||
|         return username.empty(); | ||||
|     } | ||||
| 
 | ||||
|         nlohmann::json json = nlohmann::json::parse(reply); | ||||
|         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); | ||||
|     return username == *iter; | ||||
| } | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
|  | @ -12,13 +12,11 @@ namespace WebService { | |||
| 
 | ||||
| /**
 | ||||
|  * Checks if username and token is valid | ||||
|  * @param host the web API URL | ||||
|  * @param username Citra username to use for authentication. | ||||
|  * @param token Citra token to use for authentication. | ||||
|  * @param endpoint_url URL of the services.citra-emu.org endpoint. | ||||
|  * @param func A function that gets exectued when the verification is finished | ||||
|  * @returns Future with bool indicating whether the verification succeeded | ||||
|  * @returns a bool indicating whether the verification succeeded | ||||
|  */ | ||||
| std::future<bool> VerifyLogin(std::string& username, std::string& token, | ||||
|                               const std::string& endpoint_url, std::function<void()> func); | ||||
| bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token); | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
|  | @ -20,334 +20,130 @@ constexpr int HTTPS_PORT = 443; | |||
| 
 | ||||
| constexpr int TIMEOUT_SECONDS = 30; | ||||
| 
 | ||||
| std::string UpdateCoreJWT(bool force_new_token, const std::string& username, | ||||
|                           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; | ||||
| } | ||||
| Client::JWTCache Client::jwt_cache{}; | ||||
| 
 | ||||
| std::unique_ptr<httplib::Client> GetClientFor(const LUrlParser::clParseURL& parsedUrl) { | ||||
|     namespace hl = httplib; | ||||
| 
 | ||||
|     int port; | ||||
| 
 | ||||
|     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; | ||||
| Client::Client(const std::string& host, const std::string& username, const std::string& token) | ||||
|     : host(host), username(username), token(token) { | ||||
|     std::lock_guard<std::mutex> lock(jwt_cache.mutex); | ||||
|     if (username == jwt_cache.username && token == jwt_cache.token) { | ||||
|         jwt = jwt_cache.jwt; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static Common::WebResult PostJsonAsyncFn(const std::string& url, | ||||
|                                          const LUrlParser::clParseURL& parsed_url, | ||||
|                                          const httplib::Headers& params, const std::string& data, | ||||
|                                          bool is_jwt_requested) { | ||||
|     static bool is_first_attempt = true; | ||||
| 
 | ||||
|     namespace hl = httplib; | ||||
|     std::unique_ptr<hl::Client> cli = GetClientFor(parsed_url); | ||||
| 
 | ||||
| Common::WebResult Client::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) { | ||||
|     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; | ||||
|     request.method = "POST"; | ||||
|     request.path = "/" + parsed_url.m_Path; | ||||
|     httplib::Headers params; | ||||
|     if (!jwt.empty()) { | ||||
|         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.body = data; | ||||
| 
 | ||||
|     hl::Response response; | ||||
|     httplib::Response 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"}; | ||||
|     } | ||||
| 
 | ||||
|     if (response.status >= 400) { | ||||
|         LOG_ERROR(WebService, "POST to {} returned error status code: {}", url, response.status); | ||||
|         if (response.status == 401 && !is_jwt_requested && is_first_attempt) { | ||||
|             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; | ||||
|         } | ||||
|         LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path, | ||||
|                   response.status); | ||||
|         return Common::WebResult{Common::WebResult::Code::HttpError, | ||||
|                                  std::to_string(response.status)}; | ||||
|     } | ||||
| 
 | ||||
|     auto content_type = response.headers.find("content-type"); | ||||
| 
 | ||||
|     if (content_type == response.headers.end() || | ||||
|         (content_type->second.find("application/json") == std::string::npos && | ||||
|          content_type->second.find("text/html; charset=utf-8") == std::string::npos)) { | ||||
|         LOG_ERROR(WebService, "POST to {} returned wrong content: {}", url, content_type->second); | ||||
|     if (content_type == response.headers.end()) { | ||||
|         LOG_ERROR(WebService, "{} to {} returned no content", method, host + path); | ||||
|         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}; | ||||
| } | ||||
| 
 | ||||
| std::future<Common::WebResult> PostJson(const std::string& url, const std::string& data, | ||||
|                                         bool allow_anonymous) { | ||||
| 
 | ||||
|     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, [] { | ||||
|             return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; | ||||
|         }); | ||||
| void Client::UpdateJWT() { | ||||
|     if (!username.empty() && !token.empty()) { | ||||
|         auto result = GenericJson("POST", "/jwt/internal", "", "", username, token); | ||||
|         if (result.result_code != Common::WebResult::Code::Success) { | ||||
|             LOG_ERROR(WebService, "UpdateJWT failed"); | ||||
|         } else { | ||||
|             std::lock_guard<std::mutex> lock(jwt_cache.mutex); | ||||
|             jwt_cache.username = username; | ||||
|             jwt_cache.token = token; | ||||
|             jwt_cache.jwt = jwt = result.returned_data; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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, | ||||
|                                         const std::string& token) { | ||||
|     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, [] { | ||||
|             return Common::WebResult{Common::WebResult::Code::InvalidURL, ""}; | ||||
|         }); | ||||
| Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, | ||||
|                                       const std::string& data, bool allow_anonymous) { | ||||
|     if (jwt.empty()) { | ||||
|         UpdateJWT(); | ||||
|     } | ||||
| 
 | ||||
|     const bool are_credentials_provided{!token.empty() && !username.empty()}; | ||||
|     if (!are_credentials_provided) { | ||||
|     if (jwt.empty() && !allow_anonymous) { | ||||
|         LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); | ||||
|         return std::async(std::launch::deferred, [] { | ||||
|             return Common::WebResult{Common::WebResult::Code::CredentialsMissing, ""}; | ||||
|         }); | ||||
|         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("x-username"), username}, | ||||
|                   {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")}}; | ||||
|     auto result = GenericJson(method, path, data, jwt); | ||||
|     if (result.result_string == "401") { | ||||
|         // Try again with new JWT
 | ||||
|         UpdateJWT(); | ||||
|         result = GenericJson(method, path, data, jwt); | ||||
|     } | ||||
| 
 | ||||
|     // Post JSON asynchronously
 | ||||
|     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; | ||||
|     }); | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
|  | @ -5,79 +5,88 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <functional> | ||||
| #include <future> | ||||
| #include <mutex> | ||||
| #include <string> | ||||
| #include <tuple> | ||||
| #include <httplib.h> | ||||
| #include "common/announce_multiplayer_room.h" | ||||
| #include "common/common_types.h" | ||||
| 
 | ||||
| namespace LUrlParser { | ||||
| class clParseURL; | ||||
| namespace httplib { | ||||
| class Client; | ||||
| } | ||||
| 
 | ||||
| namespace WebService { | ||||
| 
 | ||||
| /**
 | ||||
|  * Requests a new JWT if necessary | ||||
|  * @param force_new_token If true, force to request a new token from the server. | ||||
|  * @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); | ||||
| class Client { | ||||
| public: | ||||
|     Client(const std::string& host, const std::string& username, const std::string& token); | ||||
| 
 | ||||
| /**
 | ||||
|  * Posts JSON to a api.citra-emu.org. | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @param parsed_url Parsed URL used for the POST request. | ||||
|  * @param params Headers sent for the POST request. | ||||
|  * @param data String of JSON data to use for the body of the POST request. | ||||
|  * @param data If true, a JWT is requested in the function | ||||
|  * @return future with the returned value of the POST | ||||
|  */ | ||||
| static Common::WebResult PostJsonAsyncFn(const std::string& url, | ||||
|                                          const LUrlParser::clParseURL& parsed_url, | ||||
|                                          const httplib::Headers& params, const std::string& data, | ||||
|                                          bool is_jwt_requested); | ||||
|     /**
 | ||||
|      * Posts JSON to the specified path. | ||||
|      * @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. | ||||
|      * @return the result of the request. | ||||
|      */ | ||||
|     Common::WebResult PostJson(const std::string& path, const std::string& data, | ||||
|                                bool allow_anonymous) { | ||||
|         return GenericJson("POST", path, data, allow_anonymous); | ||||
|     } | ||||
| 
 | ||||
| /**
 | ||||
|  * Posts JSON to api.citra-emu.org. | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @param data String of JSON data to use for the body of the POST request. | ||||
|  * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||||
|  * @return future with the returned value of the POST | ||||
|  */ | ||||
| std::future<Common::WebResult> PostJson(const std::string& url, const std::string& data, | ||||
|                                         bool allow_anonymous); | ||||
|     /**
 | ||||
|      * Gets JSON from the specified path. | ||||
|      * @param path the URL segment after the host address. | ||||
|      * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||||
|      * @return the result of the request. | ||||
|      */ | ||||
|     Common::WebResult GetJson(const std::string& path, bool allow_anonymous) { | ||||
|         return GenericJson("GET", path, "", allow_anonymous); | ||||
|     } | ||||
| 
 | ||||
| /**
 | ||||
|  * Posts JSON to api.citra-emu.org. | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @param username Citra username to use for authentication. | ||||
|  * @param token Citra token to use for authentication. | ||||
|  * @return future with the error or result of the POST | ||||
|  */ | ||||
| std::future<Common::WebResult> PostJson(const std::string& url, const std::string& username, | ||||
|                                         const std::string& token); | ||||
|     /**
 | ||||
|      * Deletes JSON to the specified path. | ||||
|      * @param path the URL segment after the host address. | ||||
|      * @param data String of JSON data to use for the body of the DELETE request. | ||||
|      * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||||
|      * @return the result of the request. | ||||
|      */ | ||||
|     Common::WebResult DeleteJson(const std::string& path, const std::string& data, | ||||
|                                  bool allow_anonymous) { | ||||
|         return GenericJson("DELETE", path, data, allow_anonymous); | ||||
|     } | ||||
| 
 | ||||
| /**
 | ||||
|  * Gets JSON from api.citra-emu.org. | ||||
|  * @param func A function that gets exectued when the json as a string is received | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @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); | ||||
| private: | ||||
|     /// A generic function handles POST, GET and DELETE request together
 | ||||
|     Common::WebResult GenericJson(const std::string& method, const std::string& path, | ||||
|                                   const std::string& data, bool allow_anonymous); | ||||
| 
 | ||||
| /**
 | ||||
|  * Delete JSON to api.citra-emu.org. | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @param data String of JSON data to use for the body of the DELETE request. | ||||
|  */ | ||||
| void DeleteJson(const std::string& url, const std::string& data); | ||||
|     /**
 | ||||
|      * A generic function with explicit authentication method specified | ||||
|      * JWT is used if the jwt parameter is not empty | ||||
|      * 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 | ||||
|      */ | ||||
|     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
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue