From 28c726f20545744a3052a3e8a0a3bf5ff95a5042 Mon Sep 17 00:00:00 2001 From: B3n30 Date: Tue, 19 Sep 2017 03:18:26 +0200 Subject: [PATCH] WebService: Verify username and token (#2930) * WebService: Verify username and token; Log errors in PostJson * Fixup: added docstrings to the functions * Webservice: Added Icons to the verification, imrpved error detection in cpr, fixup nits * fixup: fmt warning --- dist/icons/checked.png | Bin 0 -> 451 bytes dist/icons/failed.png | Bin 0 -> 428 bytes dist/icons/icons.qrc | 6 ++ src/citra/config.cpp | 2 + src/citra/default_ini.h | 2 + src/citra_qt/CMakeLists.txt | 5 +- src/citra_qt/configuration/config.cpp | 6 ++ src/citra_qt/configuration/configure_web.cpp | 58 ++++++++++- src/citra_qt/configuration/configure_web.h | 12 ++- src/citra_qt/configuration/configure_web.ui | 75 ++++++++++---- src/core/settings.h | 1 + src/core/telemetry_session.cpp | 12 +++ src/core/telemetry_session.h | 10 ++ src/web_service/CMakeLists.txt | 2 + src/web_service/verify_login.cpp | 28 +++++ src/web_service/verify_login.h | 24 +++++ src/web_service/web_backend.cpp | 101 ++++++++++++++++--- src/web_service/web_backend.h | 16 +++ 18 files changed, 322 insertions(+), 38 deletions(-) create mode 100644 dist/icons/checked.png create mode 100644 dist/icons/failed.png create mode 100644 dist/icons/icons.qrc create mode 100644 src/web_service/verify_login.cpp create mode 100644 src/web_service/verify_login.h diff --git a/dist/icons/checked.png b/dist/icons/checked.png new file mode 100644 index 0000000000000000000000000000000000000000..c277e6b4043b2b9a7c7388e076d2c493a23272c7 GIT binary patch literal 451 zcmV;!0X+VRP)vS{GWXg z=jFEi{#(+mPYdl{bJ+sm-!h2X0+2YXqDA3r`Z z{0G?zlwk(Ok%)vW!-P4vv1&#KAea1K`JDUz+#3vF3}U;W`I!(fb_Hr!{gNAuT}jjo tH^dVdGoGE~X@(m@tY$_^vok<|0RUEP!L?xlWyb&j002ovPDHLkV1gj@#Tx(s literal 0 HcmV?d00001 diff --git a/dist/icons/failed.png b/dist/icons/failed.png new file mode 100644 index 0000000000000000000000000000000000000000..ac10f174a6016ce67b9584762866248331104a15 GIT binary patch literal 428 zcmV;d0aN~oP)iBJ(9{^)nYy>P1iS9e6X z!PkBz6qq{~ED9X_H1H;RLik$z*jdv8-<+vR4(t%bzxCJw@6ECkn6Hn4HXv8u123st z4YRt$xL&m1m#x)EKlwbxosNdtMuRAdG>*GnO7@h!9rznWTT6*Lf%)fhz?&5v>}cF0000u( literal 0 HcmV?d00001 diff --git a/dist/icons/icons.qrc b/dist/icons/icons.qrc new file mode 100644 index 000000000..f0c44862f --- /dev/null +++ b/dist/icons/icons.qrc @@ -0,0 +1,6 @@ + + + checked.png + failed.png + + diff --git a/src/citra/config.cpp b/src/citra/config.cpp index a48ef08c7..45c28ad09 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -162,6 +162,8 @@ void Config::ReadValues() { sdl2_config->GetBoolean("WebService", "enable_telemetry", true); Settings::values.telemetry_endpoint_url = sdl2_config->Get( "WebService", "telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry"); + Settings::values.verify_endpoint_url = sdl2_config->Get( + "WebService", "verify_endpoint_url", "https://services.citra-emu.org/api/profile"); Settings::values.citra_username = sdl2_config->Get("WebService", "citra_username", ""); Settings::values.citra_token = sdl2_config->Get("WebService", "citra_token", ""); } diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 4b13a2e1b..59faf773f 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -185,6 +185,8 @@ gdbstub_port=24689 enable_telemetry = # Endpoint URL for submitting telemetry data telemetry_endpoint_url = https://services.citra-emu.org/api/telemetry +# Endpoint URL to verify the username and token +verify_endpoint_url = https://services.citra-emu.org/api/profile # Username and token for Citra Web Service # See https://services.citra-emu.org/ for more info citra_username = diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index e0a19fd9e..add7566c2 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -79,6 +79,7 @@ set(UIS main.ui ) +file(GLOB_RECURSE ICONS ${CMAKE_SOURCE_DIR}/dist/icons/*) file(GLOB_RECURSE THEMES ${CMAKE_SOURCE_DIR}/dist/qt_themes/*) create_directory_groups(${SRCS} ${HEADERS} ${UIS}) @@ -92,10 +93,10 @@ endif() if (APPLE) set(MACOSX_ICON "../../dist/citra.icns") set_source_files_properties(${MACOSX_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - add_executable(citra-qt MACOSX_BUNDLE ${SRCS} ${HEADERS} ${UI_HDRS} ${THEMES} ${MACOSX_ICON}) + add_executable(citra-qt MACOSX_BUNDLE ${SRCS} ${HEADERS} ${UI_HDRS} ${ICONS} ${THEMES} ${MACOSX_ICON}) set_target_properties(citra-qt PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist) else() - add_executable(citra-qt ${SRCS} ${HEADERS} ${UI_HDRS} ${THEMES}) + add_executable(citra-qt ${SRCS} ${HEADERS} ${UI_HDRS} ${ICONS} ${THEMES}) endif() target_link_libraries(citra-qt PRIVATE audio_core common core input_common network video_core) target_link_libraries(citra-qt PRIVATE Boost::boost glad nihstro-headers Qt5::OpenGL Qt5::Widgets) diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index ef114aad3..5261f4c4c 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -146,6 +146,10 @@ void Config::ReadValues() { qt_config->value("telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry") .toString() .toStdString(); + Settings::values.verify_endpoint_url = + qt_config->value("verify_endpoint_url", "https://services.citra-emu.org/api/profile") + .toString() + .toStdString(); Settings::values.citra_username = qt_config->value("citra_username").toString().toStdString(); Settings::values.citra_token = qt_config->value("citra_token").toString().toStdString(); qt_config->endGroup(); @@ -293,6 +297,8 @@ void Config::SaveValues() { qt_config->setValue("enable_telemetry", Settings::values.enable_telemetry); qt_config->setValue("telemetry_endpoint_url", QString::fromStdString(Settings::values.telemetry_endpoint_url)); + qt_config->setValue("verify_endpoint_url", + QString::fromStdString(Settings::values.verify_endpoint_url)); qt_config->setValue("citra_username", QString::fromStdString(Settings::values.citra_username)); qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token)); qt_config->endGroup(); diff --git a/src/citra_qt/configuration/configure_web.cpp b/src/citra_qt/configuration/configure_web.cpp index 8715fb018..38ce19c0f 100644 --- a/src/citra_qt/configuration/configure_web.cpp +++ b/src/citra_qt/configuration/configure_web.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include "citra_qt/configuration/configure_web.h" #include "core/settings.h" #include "core/telemetry_session.h" @@ -11,7 +12,9 @@ ConfigureWeb::ConfigureWeb(QWidget* parent) : QWidget(parent), ui(std::make_unique()) { ui->setupUi(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(this, &ConfigureWeb::LoginVerified, this, &ConfigureWeb::OnLoginVerified); this->setConfiguration(); } @@ -34,19 +37,66 @@ void ConfigureWeb::setConfiguration() { ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry); ui->edit_username->setText(QString::fromStdString(Settings::values.citra_username)); ui->edit_token->setText(QString::fromStdString(Settings::values.citra_token)); + // Connect after setting the values, to avoid calling OnLoginChanged now + connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); + connect(ui->edit_username, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); ui->label_telemetry_id->setText("Telemetry ID: 0x" + QString::number(Core::GetTelemetryId(), 16).toUpper()); + user_verified = true; } void ConfigureWeb::applyConfiguration() { Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked(); - Settings::values.citra_username = ui->edit_username->text().toStdString(); - Settings::values.citra_token = ui->edit_token->text().toStdString(); + if (user_verified) { + Settings::values.citra_username = ui->edit_username->text().toStdString(); + Settings::values.citra_token = ui->edit_token->text().toStdString(); + } else { + QMessageBox::warning(this, tr("Username and token not verfied"), + tr("Username and token were not verified. The changes to your " + "username and/or token have not been saved.")); + } Settings::Apply(); } -void ConfigureWeb::refreshTelemetryID() { +void ConfigureWeb::RefreshTelemetryID() { const u64 new_telemetry_id{Core::RegenerateTelemetryId()}; ui->label_telemetry_id->setText("Telemetry ID: 0x" + QString::number(new_telemetry_id, 16).toUpper()); } + +void ConfigureWeb::OnLoginChanged() { + if (ui->edit_username->text().isEmpty() && ui->edit_token->text().isEmpty()) { + user_verified = true; + ui->label_username_verified->setPixmap(QPixmap(":/icons/checked.png")); + ui->label_token_verified->setPixmap(QPixmap(":/icons/checked.png")); + } else { + user_verified = false; + ui->label_username_verified->setPixmap(QPixmap(":/icons/failed.png")); + ui->label_token_verified->setPixmap(QPixmap(":/icons/failed.png")); + } +} + +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")); +} + +void ConfigureWeb::OnLoginVerified() { + ui->button_verify_login->setEnabled(true); + ui->button_verify_login->setText(tr("Verify")); + if (verified.get()) { + user_verified = true; + ui->label_username_verified->setPixmap(QPixmap(":/icons/checked.png")); + ui->label_token_verified->setPixmap(QPixmap(":/icons/checked.png")); + } else { + ui->label_username_verified->setPixmap(QPixmap(":/icons/failed.png")); + ui->label_token_verified->setPixmap(QPixmap(":/icons/failed.png")); + QMessageBox::critical( + this, tr("Verification failed"), + tr("Verification failed. Check that you have entered your username and token " + "correctly, and that your internet connection is working.")); + } +} diff --git a/src/citra_qt/configuration/configure_web.h b/src/citra_qt/configuration/configure_web.h index 20bc254b9..ad2d58f6e 100644 --- a/src/citra_qt/configuration/configure_web.h +++ b/src/citra_qt/configuration/configure_web.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -21,10 +22,19 @@ public: void applyConfiguration(); public slots: - void refreshTelemetryID(); + void RefreshTelemetryID(); + void OnLoginChanged(); + void VerifyLogin(); + void OnLoginVerified(); + +signals: + void LoginVerified(); private: void setConfiguration(); + bool user_verified = true; + std::future verified; + std::unique_ptr ui; }; diff --git a/src/citra_qt/configuration/configure_web.ui b/src/citra_qt/configuration/configure_web.ui index d8d283fad..dd996ab62 100644 --- a/src/citra_qt/configuration/configure_web.ui +++ b/src/citra_qt/configuration/configure_web.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 926 + 561 @@ -31,14 +31,30 @@ - - + + + + + 0 + 0 + + + + Qt::RightToLeft + - Username: + Verify - + + + + Sign up + + + + 36 @@ -52,7 +68,22 @@ - + + + + + + + + Username: + + + + + + + + 36 @@ -62,13 +93,6 @@ - - - - Sign up - - - @@ -76,6 +100,19 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -105,17 +142,17 @@ - - Telemetry ID: - + + Telemetry ID: + - 0 - 0 + 0 + 0 diff --git a/src/core/settings.h b/src/core/settings.h index 024f14666..8d78cb424 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -133,6 +133,7 @@ struct Values { // WebService bool enable_telemetry; std::string telemetry_endpoint_url; + std::string verify_endpoint_url; std::string citra_username; std::string citra_token; } extern values; diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp index 104a16cc9..ca517ff44 100644 --- a/src/core/telemetry_session.cpp +++ b/src/core/telemetry_session.cpp @@ -15,6 +15,7 @@ #ifdef ENABLE_WEB_SERVICE #include "web_service/telemetry_json.h" +#include "web_service/verify_login.h" #endif namespace Core { @@ -75,6 +76,17 @@ u64 RegenerateTelemetryId() { return new_telemetry_id; } +std::future VerifyLogin(std::string username, std::string token, std::function func) { +#ifdef ENABLE_WEB_SERVICE + return WebService::VerifyLogin(username, token, Settings::values.verify_endpoint_url, func); +#else + return std::async(std::launch::async, [func{std::move(func)}]() { + func(); + return false; + }); +#endif +} + TelemetrySession::TelemetrySession() { #ifdef ENABLE_WEB_SERVICE if (Settings::values.enable_telemetry) { diff --git a/src/core/telemetry_session.h b/src/core/telemetry_session.h index 65613daae..550c6ea2d 100644 --- a/src/core/telemetry_session.h +++ b/src/core/telemetry_session.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include "common/telemetry.h" @@ -47,4 +48,13 @@ u64 GetTelemetryId(); */ u64 RegenerateTelemetryId(); +/** + * Verifies the username and token. + * @param username Citra username to use for authentication. + * @param token Citra token to use for authentication. + * @param func A function that gets exectued when the verification is finished + * @returns Future with bool indicating whether the verification succeeded + */ +std::future VerifyLogin(std::string username, std::string token, std::function func); + } // namespace Core diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index 334d82a8a..c93811892 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -1,10 +1,12 @@ set(SRCS telemetry_json.cpp + verify_login.cpp web_backend.cpp ) set(HEADERS telemetry_json.h + verify_login.h web_backend.h ) diff --git a/src/web_service/verify_login.cpp b/src/web_service/verify_login.cpp new file mode 100644 index 000000000..1bc3b5afe --- /dev/null +++ b/src/web_service/verify_login.cpp @@ -0,0 +1,28 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "web_service/verify_login.h" +#include "web_service/web_backend.h" + +namespace WebService { + +std::future VerifyLogin(std::string& username, std::string& token, + const std::string& endpoint_url, std::function func) { + auto get_func = [func, username](const std::string& reply) -> bool { + func(); + if (reply.empty()) + return false; + nlohmann::json json = nlohmann::json::parse(reply); + std::string result; + try { + result = json["username"]; + } catch (const nlohmann::detail::out_of_range&) { + } + return result == username; + }; + return GetJson(get_func, endpoint_url, false, username, token); +} + +} // namespace WebService diff --git a/src/web_service/verify_login.h b/src/web_service/verify_login.h new file mode 100644 index 000000000..303f5dbbc --- /dev/null +++ b/src/web_service/verify_login.h @@ -0,0 +1,24 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +namespace WebService { + +/** + * Checks if username and token is valid + * @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 + */ +std::future VerifyLogin(std::string& username, std::string& token, + const std::string& endpoint_url, std::function func); + +} // namespace WebService diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index d28a3f757..b17d82f9c 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -18,6 +18,19 @@ static constexpr char API_VERSION[]{"1"}; static std::unique_ptr g_session; +void Win32WSAStartup() { +#ifdef _WIN32 + // On Windows, CPR/libcurl does not properly initialize Winsock. The below code is used to + // initialize Winsock globally, which fixes this problem. Without this, only the first CPR + // session will properly be created, and subsequent ones will fail. + WSADATA wsa_data; + const int wsa_result{WSAStartup(MAKEWORD(2, 2), &wsa_data)}; + if (wsa_result) { + LOG_CRITICAL(WebService, "WSAStartup failed: %d", wsa_result); + } +#endif +} + void PostJson(const std::string& url, const std::string& data, bool allow_anonymous, const std::string& username, const std::string& token) { if (url.empty()) { @@ -31,16 +44,7 @@ void PostJson(const std::string& url, const std::string& data, bool allow_anonym return; } -#ifdef _WIN32 - // On Windows, CPR/libcurl does not properly initialize Winsock. The below code is used to - // initialize Winsock globally, which fixes this problem. Without this, only the first CPR - // session will properly be created, and subsequent ones will fail. - WSADATA wsa_data; - const int wsa_result{WSAStartup(MAKEWORD(2, 2), &wsa_data)}; - if (wsa_result) { - LOG_CRITICAL(WebService, "WSAStartup failed: %d", wsa_result); - } -#endif + Win32WSAStartup(); // Built request header cpr::Header header; @@ -56,8 +60,81 @@ void PostJson(const std::string& url, const std::string& data, bool allow_anonym } // Post JSON asynchronously - static cpr::AsyncResponse future; - future = cpr::PostAsync(cpr::Url{url.c_str()}, cpr::Body{data.c_str()}, header); + static std::future future; + future = cpr::PostCallback( + [](cpr::Response r) { + if (r.error) { + LOG_ERROR(WebService, "POST returned cpr error: %u:%s", + static_cast(r.error.code), r.error.message.c_str()); + return; + } + if (r.status_code >= 400) { + LOG_ERROR(WebService, "POST returned error status code: %u", r.status_code); + return; + } + if (r.header["content-type"].find("application/json") == std::string::npos) { + LOG_ERROR(WebService, "POST returned wrong content: %s", + r.header["content-type"].c_str()); + return; + } + }, + cpr::Url{url}, cpr::Body{data}, header); } +template +std::future GetJson(std::function func, const std::string& url, + bool allow_anonymous, const std::string& username, + const std::string& token) { + if (url.empty()) { + LOG_ERROR(WebService, "URL is invalid"); + return std::async(std::launch::async, [func{std::move(func)}]() { return func(""); }); + } + + const bool are_credentials_provided{!token.empty() && !username.empty()}; + if (!allow_anonymous && !are_credentials_provided) { + LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); + return std::async(std::launch::async, [func{std::move(func)}]() { return func(""); }); + } + + Win32WSAStartup(); + + // Built request header + cpr::Header header; + if (are_credentials_provided) { + // Authenticated request if credentials are provided + header = {{"Content-Type", "application/json"}, + {"x-username", username.c_str()}, + {"x-token", token.c_str()}, + {"api-version", API_VERSION}}; + } else { + // Otherwise, anonymous request + header = cpr::Header{{"Content-Type", "application/json"}, {"api-version", API_VERSION}}; + } + + // Get JSON asynchronously + return cpr::GetCallback( + [func{std::move(func)}](cpr::Response r) { + if (r.error) { + LOG_ERROR(WebService, "GET returned cpr error: %u:%s", + static_cast(r.error.code), r.error.message.c_str()); + return func(""); + } + if (r.status_code >= 400) { + LOG_ERROR(WebService, "GET returned error code: %u", r.status_code); + return func(""); + } + if (r.header["content-type"].find("application/json") == std::string::npos) { + LOG_ERROR(WebService, "GET returned wrong content: %s", + r.header["content-type"].c_str()); + return func(""); + } + return func(r.text); + }, + cpr::Url{url}, header); +} + +template std::future GetJson(std::function func, + const std::string& url, bool allow_anonymous, + const std::string& username, const std::string& token); + } // namespace WebService diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h index d17100398..a63c75d13 100644 --- a/src/web_service/web_backend.h +++ b/src/web_service/web_backend.h @@ -4,6 +4,8 @@ #pragma once +#include +#include #include #include "common/common_types.h" @@ -20,4 +22,18 @@ namespace WebService { void PostJson(const std::string& url, const std::string& data, bool allow_anonymous, const std::string& username = {}, const std::string& token = {}); +/** + * Gets JSON from services.citra-emu.org. + * @param func A function that gets exectued when the json as a string is received + * @param url URL of the services.citra-emu.org endpoint to post data to. + * @param allow_anonymous If true, allow anonymous unauthenticated requests. + * @param username Citra username to use for authentication. + * @param token Citra token to use for authentication. + * @return future that holds the return value T of the func + */ +template +std::future GetJson(std::function func, const std::string& url, + bool allow_anonymous, const std::string& username = {}, + const std::string& token = {}); + } // namespace WebService