Merge pull request #3085 from bunnei/web-token-b64
yuzu: configure_web: Use Base64 encoded token
This commit is contained in:
commit
8714d40a77
4 changed files with 110 additions and 50 deletions
|
@ -2,10 +2,12 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <LUrlParser.h>
|
#include <LUrlParser.h>
|
||||||
|
#include <fmt/format.h>
|
||||||
#include <httplib.h>
|
#include <httplib.h>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
@ -16,10 +18,10 @@ namespace WebService {
|
||||||
|
|
||||||
constexpr std::array<const char, 1> API_VERSION{'1'};
|
constexpr std::array<const char, 1> API_VERSION{'1'};
|
||||||
|
|
||||||
constexpr u32 HTTP_PORT = 80;
|
constexpr int HTTP_PORT = 80;
|
||||||
constexpr u32 HTTPS_PORT = 443;
|
constexpr int HTTPS_PORT = 443;
|
||||||
|
|
||||||
constexpr u32 TIMEOUT_SECONDS = 30;
|
constexpr std::size_t TIMEOUT_SECONDS = 30;
|
||||||
|
|
||||||
struct Client::Impl {
|
struct Client::Impl {
|
||||||
Impl(std::string host, std::string username, std::string token)
|
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
|
/// A generic function handles POST, GET and DELETE request together
|
||||||
Common::WebResult GenericJson(const std::string& method, const std::string& path,
|
Common::WebResult GenericRequest(const std::string& method, const std::string& path,
|
||||||
const std::string& data, bool allow_anonymous) {
|
const std::string& data, bool allow_anonymous,
|
||||||
|
const std::string& accept) {
|
||||||
if (jwt.empty()) {
|
if (jwt.empty()) {
|
||||||
UpdateJWT();
|
UpdateJWT();
|
||||||
}
|
}
|
||||||
|
@ -43,11 +46,11 @@ struct Client::Impl {
|
||||||
"Credentials needed"};
|
"Credentials needed"};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = GenericJson(method, path, data, jwt);
|
auto result = GenericRequest(method, path, data, accept, jwt);
|
||||||
if (result.result_string == "401") {
|
if (result.result_string == "401") {
|
||||||
// Try again with new JWT
|
// Try again with new JWT
|
||||||
UpdateJWT();
|
UpdateJWT();
|
||||||
result = GenericJson(method, path, data, jwt);
|
result = GenericRequest(method, path, data, accept, jwt);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -56,12 +59,13 @@ struct Client::Impl {
|
||||||
/**
|
/**
|
||||||
* A generic function with explicit authentication method specified
|
* A generic function with explicit authentication method specified
|
||||||
* JWT is used if the jwt parameter is not empty
|
* 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
|
* username + token is used if jwt is empty but username and token are
|
||||||
* anonymous if all of jwt, username and token are empty
|
* not empty anonymous if all of jwt, username and token are empty
|
||||||
*/
|
*/
|
||||||
Common::WebResult GenericJson(const std::string& method, const std::string& path,
|
Common::WebResult GenericRequest(const std::string& method, const std::string& path,
|
||||||
const std::string& data, const std::string& jwt = "",
|
const std::string& data, const std::string& accept,
|
||||||
const std::string& username = "", const std::string& token = "") {
|
const std::string& jwt = "", const std::string& username = "",
|
||||||
|
const std::string& token = "") {
|
||||||
if (cli == nullptr) {
|
if (cli == nullptr) {
|
||||||
auto parsedUrl = LUrlParser::clParseURL::ParseURL(host);
|
auto parsedUrl = LUrlParser::clParseURL::ParseURL(host);
|
||||||
int port;
|
int port;
|
||||||
|
@ -132,8 +136,7 @@ struct Client::Impl {
|
||||||
return Common::WebResult{Common::WebResult::Code::WrongContent, ""};
|
return Common::WebResult{Common::WebResult::Code::WrongContent, ""};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content_type->second.find("application/json") == std::string::npos &&
|
if (content_type->second.find(accept) == 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,
|
LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path,
|
||||||
content_type->second);
|
content_type->second);
|
||||||
return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"};
|
return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"};
|
||||||
|
@ -147,7 +150,7 @@ struct Client::Impl {
|
||||||
return;
|
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) {
|
if (result.result_code != Common::WebResult::Code::Success) {
|
||||||
LOG_ERROR(WebService, "UpdateJWT failed");
|
LOG_ERROR(WebService, "UpdateJWT failed");
|
||||||
} else {
|
} else {
|
||||||
|
@ -180,16 +183,29 @@ Client::~Client() = default;
|
||||||
|
|
||||||
Common::WebResult Client::PostJson(const std::string& path, const std::string& data,
|
Common::WebResult Client::PostJson(const std::string& path, const std::string& data,
|
||||||
bool allow_anonymous) {
|
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) {
|
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,
|
Common::WebResult Client::DeleteJson(const std::string& path, const std::string& data,
|
||||||
bool allow_anonymous) {
|
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
|
} // namespace WebService
|
||||||
|
|
|
@ -46,6 +46,29 @@ public:
|
||||||
Common::WebResult DeleteJson(const std::string& path, const std::string& data,
|
Common::WebResult DeleteJson(const std::string& path, const std::string& data,
|
||||||
bool allow_anonymous);
|
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:
|
private:
|
||||||
struct Impl;
|
struct Impl;
|
||||||
std::unique_ptr<Impl> impl;
|
std::unique_ptr<Impl> impl;
|
||||||
|
|
|
@ -11,6 +11,31 @@
|
||||||
#include "yuzu/configuration/configure_web.h"
|
#include "yuzu/configuration/configure_web.h"
|
||||||
#include "yuzu/uisettings.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)
|
ConfigureWeb::ConfigureWeb(QWidget* parent)
|
||||||
: QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) {
|
: QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) {
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
@ -63,13 +88,18 @@ void ConfigureWeb::SetConfiguration() {
|
||||||
ui->web_signup_link->setOpenExternalLinks(true);
|
ui->web_signup_link->setOpenExternalLinks(true);
|
||||||
ui->web_token_info_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->toggle_telemetry->setChecked(Settings::values.enable_telemetry);
|
||||||
ui->edit_username->setText(QString::fromStdString(Settings::values.yuzu_username));
|
ui->edit_token->setText(QString::fromStdString(
|
||||||
ui->edit_token->setText(QString::fromStdString(Settings::values.yuzu_token));
|
GenerateDisplayToken(Settings::values.yuzu_username, Settings::values.yuzu_token)));
|
||||||
|
|
||||||
// Connect after setting the values, to avoid calling OnLoginChanged now
|
// Connect after setting the values, to avoid calling OnLoginChanged now
|
||||||
connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged);
|
connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged);
|
||||||
connect(ui->edit_username, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged);
|
|
||||||
|
|
||||||
user_verified = true;
|
user_verified = true;
|
||||||
|
|
||||||
|
@ -80,12 +110,13 @@ void ConfigureWeb::ApplyConfiguration() {
|
||||||
Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked();
|
Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked();
|
||||||
UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked();
|
UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked();
|
||||||
if (user_verified) {
|
if (user_verified) {
|
||||||
Settings::values.yuzu_username = ui->edit_username->text().toStdString();
|
Settings::values.yuzu_username =
|
||||||
Settings::values.yuzu_token = ui->edit_token->text().toStdString();
|
UsernameFromDisplayToken(ui->edit_token->text().toStdString());
|
||||||
|
Settings::values.yuzu_token = TokenFromDisplayToken(ui->edit_token->text().toStdString());
|
||||||
} else {
|
} else {
|
||||||
QMessageBox::warning(this, tr("Username and token not verified"),
|
QMessageBox::warning(
|
||||||
tr("Username and token were not verified. The changes to your "
|
this, tr("Token not verified"),
|
||||||
"username and/or token have not been saved."));
|
tr("Token was not verified. The change to your token has not been saved."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,17 +127,15 @@ void ConfigureWeb::RefreshTelemetryID() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureWeb::OnLoginChanged() {
|
void ConfigureWeb::OnLoginChanged() {
|
||||||
if (ui->edit_username->text().isEmpty() && ui->edit_token->text().isEmpty()) {
|
if (ui->edit_token->text().isEmpty()) {
|
||||||
user_verified = true;
|
user_verified = true;
|
||||||
|
|
||||||
const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16);
|
const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16);
|
||||||
ui->label_username_verified->setPixmap(pixmap);
|
|
||||||
ui->label_token_verified->setPixmap(pixmap);
|
ui->label_token_verified->setPixmap(pixmap);
|
||||||
} else {
|
} else {
|
||||||
user_verified = false;
|
user_verified = false;
|
||||||
|
|
||||||
const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16);
|
const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16);
|
||||||
ui->label_username_verified->setPixmap(pixmap);
|
|
||||||
ui->label_token_verified->setPixmap(pixmap);
|
ui->label_token_verified->setPixmap(pixmap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,8 +143,9 @@ void ConfigureWeb::OnLoginChanged() {
|
||||||
void ConfigureWeb::VerifyLogin() {
|
void ConfigureWeb::VerifyLogin() {
|
||||||
ui->button_verify_login->setDisabled(true);
|
ui->button_verify_login->setDisabled(true);
|
||||||
ui->button_verify_login->setText(tr("Verifying..."));
|
ui->button_verify_login->setText(tr("Verifying..."));
|
||||||
verify_watcher.setFuture(QtConcurrent::run([username = ui->edit_username->text().toStdString(),
|
verify_watcher.setFuture(QtConcurrent::run(
|
||||||
token = ui->edit_token->text().toStdString()] {
|
[username = UsernameFromDisplayToken(ui->edit_token->text().toStdString()),
|
||||||
|
token = TokenFromDisplayToken(ui->edit_token->text().toStdString())] {
|
||||||
return Core::VerifyLogin(username, token);
|
return Core::VerifyLogin(username, token);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -127,16 +157,15 @@ void ConfigureWeb::OnLoginVerified() {
|
||||||
user_verified = true;
|
user_verified = true;
|
||||||
|
|
||||||
const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16);
|
const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16);
|
||||||
ui->label_username_verified->setPixmap(pixmap);
|
|
||||||
ui->label_token_verified->setPixmap(pixmap);
|
ui->label_token_verified->setPixmap(pixmap);
|
||||||
|
ui->username->setText(
|
||||||
|
QString::fromStdString(UsernameFromDisplayToken(ui->edit_token->text().toStdString())));
|
||||||
} else {
|
} else {
|
||||||
const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16);
|
const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16);
|
||||||
ui->label_username_verified->setPixmap(pixmap);
|
|
||||||
ui->label_token_verified->setPixmap(pixmap);
|
ui->label_token_verified->setPixmap(pixmap);
|
||||||
|
ui->username->setText(tr("Unspecified"));
|
||||||
QMessageBox::critical(
|
QMessageBox::critical(this, tr("Verification failed"),
|
||||||
this, tr("Verification failed"),
|
tr("Verification failed. Check that you have entered your token "
|
||||||
tr("Verification failed. Check that you have entered your username and token "
|
|
||||||
"correctly, and that your internet connection is working."));
|
"correctly, and that your internet connection is working."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,11 +55,7 @@
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1" colspan="3">
|
<item row="0" column="1" colspan="3">
|
||||||
<widget class="QLineEdit" name="edit_username">
|
<widget class="QLabel" name="username" />
|
||||||
<property name="maxLength">
|
|
||||||
<number>36</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="label_token">
|
<widget class="QLabel" name="label_token">
|
||||||
|
@ -79,14 +75,10 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="4">
|
|
||||||
<widget class="QLabel" name="label_username_verified">
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1" colspan="3">
|
<item row="1" column="1" colspan="3">
|
||||||
<widget class="QLineEdit" name="edit_token">
|
<widget class="QLineEdit" name="edit_token">
|
||||||
<property name="maxLength">
|
<property name="maxLength">
|
||||||
<number>36</number>
|
<number>80</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="echoMode">
|
<property name="echoMode">
|
||||||
<enum>QLineEdit::Password</enum>
|
<enum>QLineEdit::Password</enum>
|
||||||
|
|
Loading…
Reference in a new issue