diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/citra/config.cpp | 2 | ||||
-rw-r--r-- | src/citra/default_ini.h | 2 | ||||
-rw-r--r-- | src/citra_qt/CMakeLists.txt | 5 | ||||
-rw-r--r-- | src/citra_qt/configuration/config.cpp | 6 | ||||
-rw-r--r-- | src/citra_qt/configuration/configure_web.cpp | 58 | ||||
-rw-r--r-- | src/citra_qt/configuration/configure_web.h | 12 | ||||
-rw-r--r-- | src/citra_qt/configuration/configure_web.ui | 75 | ||||
-rw-r--r-- | src/core/settings.h | 1 | ||||
-rw-r--r-- | src/core/telemetry_session.cpp | 12 | ||||
-rw-r--r-- | src/core/telemetry_session.h | 10 | ||||
-rw-r--r-- | src/web_service/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/web_service/verify_login.cpp | 28 | ||||
-rw-r--r-- | src/web_service/verify_login.h | 24 | ||||
-rw-r--r-- | src/web_service/web_backend.cpp | 101 | ||||
-rw-r--r-- | src/web_service/web_backend.h | 16 |
15 files changed, 316 insertions, 38 deletions
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 <QMessageBox> #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::ConfigureWeb>()) { 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 <future> #include <memory> #include <QWidget> @@ -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<bool> verified; + std::unique_ptr<Ui::ConfigureWeb> 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 @@ <rect> <x>0</x> <y>0</y> - <width>400</width> - <height>300</height> + <width>926</width> + <height>561</height> </rect> </property> <property name="windowTitle"> @@ -31,14 +31,30 @@ </item> <item> <layout class="QGridLayout" name="gridLayoutCitraUsername"> - <item row="0" column="0"> - <widget class="QLabel" name="label_username"> + <item row="2" column="3"> + <widget class="QPushButton" name="button_verify_login"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="layoutDirection"> + <enum>Qt::RightToLeft</enum> + </property> <property name="text"> - <string>Username: </string> + <string>Verify</string> </property> </widget> </item> - <item row="0" column="1"> + <item row="2" column="0"> + <widget class="QLabel" name="web_signup_link"> + <property name="text"> + <string>Sign up</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="3"> <widget class="QLineEdit" name="edit_username"> <property name="maxLength"> <number>36</number> @@ -52,7 +68,22 @@ </property> </widget> </item> - <item row="1" column="1"> + <item row="1" column="4"> + <widget class="QLabel" name="label_token_verified"> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_username"> + <property name="text"> + <string>Username: </string> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QLabel" name="label_username_verified"> + </widget> + </item> + <item row="1" column="1" colspan="3"> <widget class="QLineEdit" name="edit_token"> <property name="maxLength"> <number>36</number> @@ -62,13 +93,6 @@ </property> </widget> </item> - <item row="2" column="0"> - <widget class="QLabel" name="web_signup_link"> - <property name="text"> - <string>Sign up</string> - </property> - </widget> - </item> <item row="2" column="1"> <widget class="QLabel" name="web_token_info_link"> <property name="text"> @@ -76,6 +100,19 @@ </property> </widget> </item> + <item row="2" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> </layout> </item> </layout> @@ -105,17 +142,17 @@ <layout class="QGridLayout" name="gridLayoutTelemetryId"> <item row="0" column="0"> <widget class="QLabel" name="label_telemetry_id"> - <property name="text"> - <string>Telemetry ID:</string> - </property> + <property name="text"> + <string>Telemetry ID:</string> + </property> </widget> </item> <item row="0" column="1"> <widget class="QPushButton" name="button_regenerate_telemetry_id"> <property name="sizePolicy"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> + <horstretch>0</horstretch> + <verstretch>0</verstretch> </sizepolicy> </property> <property name="layoutDirection"> 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<bool> VerifyLogin(std::string username, std::string token, std::function<void()> 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 <future> #include <memory> #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<bool> VerifyLogin(std::string username, std::string token, std::function<void()> 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 <json.hpp> +#include "web_service/verify_login.h" +#include "web_service/web_backend.h" + +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(); + 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<bool>(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 <functional> +#include <future> +#include <string> + +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<bool> VerifyLogin(std::string& username, std::string& token, + const std::string& endpoint_url, std::function<void()> 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<cpr::Session> 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<void> future; + future = cpr::PostCallback( + [](cpr::Response r) { + if (r.error) { + LOG_ERROR(WebService, "POST returned cpr error: %u:%s", + static_cast<u32>(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 <typename T> +std::future<T> GetJson(std::function<T(const std::string&)> 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<u32>(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<bool> GetJson(std::function<bool(const std::string&)> 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 <functional> +#include <future> #include <string> #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 <typename T> +std::future<T> GetJson(std::function<T(const std::string&)> func, const std::string& url, + bool allow_anonymous, const std::string& username = {}, + const std::string& token = {}); + } // namespace WebService |