mirror of
				https://git.suyu.dev/suyu/suyu
				synced 2025-10-31 16:09:03 -05:00 
			
		
		
		
	Merge pull request #3085 from bunnei/web-token-b64
yuzu: configure_web: Use Base64 encoded token
This commit is contained in:
		| @@ -2,10 +2,12 @@ | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <array> | ||||
| #include <cstdlib> | ||||
| #include <mutex> | ||||
| #include <string> | ||||
| #include <LUrlParser.h> | ||||
| #include <fmt/format.h> | ||||
| #include <httplib.h> | ||||
| #include "common/common_types.h" | ||||
| #include "common/logging/log.h" | ||||
| @@ -16,10 +18,10 @@ namespace WebService { | ||||
|  | ||||
| constexpr std::array<const char, 1> API_VERSION{'1'}; | ||||
|  | ||||
| constexpr u32 HTTP_PORT = 80; | ||||
| constexpr u32 HTTPS_PORT = 443; | ||||
| constexpr int HTTP_PORT = 80; | ||||
| constexpr int HTTPS_PORT = 443; | ||||
|  | ||||
| constexpr u32 TIMEOUT_SECONDS = 30; | ||||
| constexpr std::size_t TIMEOUT_SECONDS = 30; | ||||
|  | ||||
| struct Client::Impl { | ||||
|     Impl(std::string host, std::string username, std::string token) | ||||
| @@ -31,8 +33,9 @@ struct Client::Impl { | ||||
|     } | ||||
|  | ||||
|     /// 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) { | ||||
|     Common::WebResult GenericRequest(const std::string& method, const std::string& path, | ||||
|                                      const std::string& data, bool allow_anonymous, | ||||
|                                      const std::string& accept) { | ||||
|         if (jwt.empty()) { | ||||
|             UpdateJWT(); | ||||
|         } | ||||
| @@ -43,11 +46,11 @@ struct Client::Impl { | ||||
|                                      "Credentials needed"}; | ||||
|         } | ||||
|  | ||||
|         auto result = GenericJson(method, path, data, jwt); | ||||
|         auto result = GenericRequest(method, path, data, accept, jwt); | ||||
|         if (result.result_string == "401") { | ||||
|             // Try again with new JWT | ||||
|             UpdateJWT(); | ||||
|             result = GenericJson(method, path, data, jwt); | ||||
|             result = GenericRequest(method, path, data, accept, jwt); | ||||
|         } | ||||
|  | ||||
|         return result; | ||||
| @@ -56,12 +59,13 @@ struct Client::Impl { | ||||
|     /** | ||||
|      * 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 | ||||
|      * 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 = "") { | ||||
|     Common::WebResult GenericRequest(const std::string& method, const std::string& path, | ||||
|                                      const std::string& data, const std::string& accept, | ||||
|                                      const std::string& jwt = "", const std::string& username = "", | ||||
|                                      const std::string& token = "") { | ||||
|         if (cli == nullptr) { | ||||
|             auto parsedUrl = LUrlParser::clParseURL::ParseURL(host); | ||||
|             int port; | ||||
| @@ -132,8 +136,7 @@ struct Client::Impl { | ||||
|             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) { | ||||
|         if (content_type->second.find(accept) == 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"}; | ||||
| @@ -147,7 +150,7 @@ struct Client::Impl { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         auto result = GenericJson("POST", "/jwt/internal", "", "", username, token); | ||||
|         auto result = GenericRequest("POST", "/jwt/internal", "", "text/html", "", username, token); | ||||
|         if (result.result_code != Common::WebResult::Code::Success) { | ||||
|             LOG_ERROR(WebService, "UpdateJWT failed"); | ||||
|         } else { | ||||
| @@ -180,16 +183,29 @@ Client::~Client() = default; | ||||
|  | ||||
| Common::WebResult Client::PostJson(const std::string& path, const std::string& data, | ||||
|                                    bool allow_anonymous) { | ||||
|     return impl->GenericJson("POST", path, data, allow_anonymous); | ||||
|     return impl->GenericRequest("POST", path, data, allow_anonymous, "application/json"); | ||||
| } | ||||
|  | ||||
| Common::WebResult Client::GetJson(const std::string& path, bool allow_anonymous) { | ||||
|     return impl->GenericJson("GET", path, "", allow_anonymous); | ||||
|     return impl->GenericRequest("GET", path, "", allow_anonymous, "application/json"); | ||||
| } | ||||
|  | ||||
| Common::WebResult Client::DeleteJson(const std::string& path, const std::string& data, | ||||
|                                      bool allow_anonymous) { | ||||
|     return impl->GenericJson("DELETE", path, data, allow_anonymous); | ||||
|     return impl->GenericRequest("DELETE", path, data, allow_anonymous, "application/json"); | ||||
| } | ||||
|  | ||||
| Common::WebResult Client::GetPlain(const std::string& path, bool allow_anonymous) { | ||||
|     return impl->GenericRequest("GET", path, "", allow_anonymous, "text/plain"); | ||||
| } | ||||
|  | ||||
| Common::WebResult Client::GetImage(const std::string& path, bool allow_anonymous) { | ||||
|     return impl->GenericRequest("GET", path, "", allow_anonymous, "image/png"); | ||||
| } | ||||
|  | ||||
| Common::WebResult Client::GetExternalJWT(const std::string& audience) { | ||||
|     return impl->GenericRequest("POST", fmt::format("/jwt/external/{}", audience), "", false, | ||||
|                                 "text/html"); | ||||
| } | ||||
|  | ||||
| } // namespace WebService | ||||
|   | ||||
| @@ -46,6 +46,29 @@ public: | ||||
|     Common::WebResult DeleteJson(const std::string& path, const std::string& data, | ||||
|                                  bool allow_anonymous); | ||||
|  | ||||
|     /** | ||||
|      * Gets a plain string 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 GetPlain(const std::string& path, bool allow_anonymous); | ||||
|  | ||||
|     /** | ||||
|      * Gets an PNG image 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 GetImage(const std::string& path, bool allow_anonymous); | ||||
|  | ||||
|     /** | ||||
|      * Requests an external JWT for the specific audience provided. | ||||
|      * @param audience the audience of the JWT requested. | ||||
|      * @return the result of the request. | ||||
|      */ | ||||
|     Common::WebResult GetExternalJWT(const std::string& audience); | ||||
|  | ||||
| private: | ||||
|     struct Impl; | ||||
|     std::unique_ptr<Impl> impl; | ||||
|   | ||||
| @@ -11,6 +11,31 @@ | ||||
| #include "yuzu/configuration/configure_web.h" | ||||
| #include "yuzu/uisettings.h" | ||||
|  | ||||
| static constexpr char token_delimiter{':'}; | ||||
|  | ||||
| static std::string GenerateDisplayToken(const std::string& username, const std::string& token) { | ||||
|     if (username.empty() || token.empty()) { | ||||
|         return {}; | ||||
|     } | ||||
|  | ||||
|     const std::string unencoded_display_token{username + token_delimiter + token}; | ||||
|     QByteArray b{unencoded_display_token.c_str()}; | ||||
|     QByteArray b64 = b.toBase64(); | ||||
|     return b64.toStdString(); | ||||
| } | ||||
|  | ||||
| static std::string UsernameFromDisplayToken(const std::string& display_token) { | ||||
|     const std::string unencoded_display_token{ | ||||
|         QByteArray::fromBase64(display_token.c_str()).toStdString()}; | ||||
|     return unencoded_display_token.substr(0, unencoded_display_token.find(token_delimiter)); | ||||
| } | ||||
|  | ||||
| static std::string TokenFromDisplayToken(const std::string& display_token) { | ||||
|     const std::string unencoded_display_token{ | ||||
|         QByteArray::fromBase64(display_token.c_str()).toStdString()}; | ||||
|     return unencoded_display_token.substr(unencoded_display_token.find(token_delimiter) + 1); | ||||
| } | ||||
|  | ||||
| ConfigureWeb::ConfigureWeb(QWidget* parent) | ||||
|     : QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) { | ||||
|     ui->setupUi(this); | ||||
| @@ -63,13 +88,18 @@ void ConfigureWeb::SetConfiguration() { | ||||
|     ui->web_signup_link->setOpenExternalLinks(true); | ||||
|     ui->web_token_info_link->setOpenExternalLinks(true); | ||||
|  | ||||
|     if (Settings::values.yuzu_username.empty()) { | ||||
|         ui->username->setText(tr("Unspecified")); | ||||
|     } else { | ||||
|         ui->username->setText(QString::fromStdString(Settings::values.yuzu_username)); | ||||
|     } | ||||
|  | ||||
|     ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry); | ||||
|     ui->edit_username->setText(QString::fromStdString(Settings::values.yuzu_username)); | ||||
|     ui->edit_token->setText(QString::fromStdString(Settings::values.yuzu_token)); | ||||
|     ui->edit_token->setText(QString::fromStdString( | ||||
|         GenerateDisplayToken(Settings::values.yuzu_username, Settings::values.yuzu_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); | ||||
|  | ||||
|     user_verified = true; | ||||
|  | ||||
| @@ -80,12 +110,13 @@ void ConfigureWeb::ApplyConfiguration() { | ||||
|     Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked(); | ||||
|     UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked(); | ||||
|     if (user_verified) { | ||||
|         Settings::values.yuzu_username = ui->edit_username->text().toStdString(); | ||||
|         Settings::values.yuzu_token = ui->edit_token->text().toStdString(); | ||||
|         Settings::values.yuzu_username = | ||||
|             UsernameFromDisplayToken(ui->edit_token->text().toStdString()); | ||||
|         Settings::values.yuzu_token = TokenFromDisplayToken(ui->edit_token->text().toStdString()); | ||||
|     } else { | ||||
|         QMessageBox::warning(this, tr("Username and token not verified"), | ||||
|                              tr("Username and token were not verified. The changes to your " | ||||
|                                 "username and/or token have not been saved.")); | ||||
|         QMessageBox::warning( | ||||
|             this, tr("Token not verified"), | ||||
|             tr("Token was not verified. The change to your token has not been saved.")); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -96,17 +127,15 @@ void ConfigureWeb::RefreshTelemetryID() { | ||||
| } | ||||
|  | ||||
| void ConfigureWeb::OnLoginChanged() { | ||||
|     if (ui->edit_username->text().isEmpty() && ui->edit_token->text().isEmpty()) { | ||||
|     if (ui->edit_token->text().isEmpty()) { | ||||
|         user_verified = true; | ||||
|  | ||||
|         const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16); | ||||
|         ui->label_username_verified->setPixmap(pixmap); | ||||
|         ui->label_token_verified->setPixmap(pixmap); | ||||
|     } else { | ||||
|         user_verified = false; | ||||
|  | ||||
|         const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16); | ||||
|         ui->label_username_verified->setPixmap(pixmap); | ||||
|         ui->label_token_verified->setPixmap(pixmap); | ||||
|     } | ||||
| } | ||||
| @@ -114,10 +143,11 @@ void ConfigureWeb::OnLoginChanged() { | ||||
| void ConfigureWeb::VerifyLogin() { | ||||
|     ui->button_verify_login->setDisabled(true); | ||||
|     ui->button_verify_login->setText(tr("Verifying...")); | ||||
|     verify_watcher.setFuture(QtConcurrent::run([username = ui->edit_username->text().toStdString(), | ||||
|                                                 token = ui->edit_token->text().toStdString()] { | ||||
|         return Core::VerifyLogin(username, token); | ||||
|     })); | ||||
|     verify_watcher.setFuture(QtConcurrent::run( | ||||
|         [username = UsernameFromDisplayToken(ui->edit_token->text().toStdString()), | ||||
|          token = TokenFromDisplayToken(ui->edit_token->text().toStdString())] { | ||||
|             return Core::VerifyLogin(username, token); | ||||
|         })); | ||||
| } | ||||
|  | ||||
| void ConfigureWeb::OnLoginVerified() { | ||||
| @@ -127,16 +157,15 @@ void ConfigureWeb::OnLoginVerified() { | ||||
|         user_verified = true; | ||||
|  | ||||
|         const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16); | ||||
|         ui->label_username_verified->setPixmap(pixmap); | ||||
|         ui->label_token_verified->setPixmap(pixmap); | ||||
|         ui->username->setText( | ||||
|             QString::fromStdString(UsernameFromDisplayToken(ui->edit_token->text().toStdString()))); | ||||
|     } else { | ||||
|         const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16); | ||||
|         ui->label_username_verified->setPixmap(pixmap); | ||||
|         ui->label_token_verified->setPixmap(pixmap); | ||||
|  | ||||
|         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.")); | ||||
|         ui->username->setText(tr("Unspecified")); | ||||
|         QMessageBox::critical(this, tr("Verification failed"), | ||||
|                               tr("Verification failed. Check that you have entered your token " | ||||
|                                  "correctly, and that your internet connection is working.")); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -55,11 +55,7 @@ | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="0" column="1" colspan="3"> | ||||
|            <widget class="QLineEdit" name="edit_username"> | ||||
|             <property name="maxLength"> | ||||
|              <number>36</number> | ||||
|             </property> | ||||
|            </widget> | ||||
|            <widget class="QLabel" name="username" /> | ||||
|           </item> | ||||
|           <item row="1" column="0"> | ||||
|            <widget class="QLabel" name="label_token"> | ||||
| @@ -79,14 +75,10 @@ | ||||
|             </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> | ||||
|              <number>80</number> | ||||
|             </property> | ||||
|             <property name="echoMode"> | ||||
|              <enum>QLineEdit::Password</enum> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 bunnei
					bunnei