Просмотр исходного кода

Merge pull request #3085 from bunnei/web-token-b64

yuzu: configure_web: Use Base64 encoded token
bunnei 6 лет назад
Родитель
Сommit
8714d40a77

+ 34 - 18
src/web_service/web_backend.cpp

@@ -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

+ 23 - 0
src/web_service/web_backend.h

@@ -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;

+ 51 - 22
src/yuzu/configuration/configure_web.cpp

@@ -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."));
     }
 }

+ 2 - 10
src/yuzu/configuration/configure_web.ui

@@ -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>