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
This commit is contained in:
B3n30 2017-09-19 03:18:26 +02:00 committed by bunnei
parent 255fd8768d
commit 28c726f205
18 changed files with 322 additions and 38 deletions

BIN
dist/icons/checked.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

BIN
dist/icons/failed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

6
dist/icons/icons.qrc vendored Normal file
View File

@ -0,0 +1,6 @@
<RCC>
<qresource prefix="icons">
<file>checked.png</file>
<file>failed.png</file>
</qresource>
</RCC>

View File

@ -162,6 +162,8 @@ void Config::ReadValues() {
sdl2_config->GetBoolean("WebService", "enable_telemetry", true); sdl2_config->GetBoolean("WebService", "enable_telemetry", true);
Settings::values.telemetry_endpoint_url = sdl2_config->Get( Settings::values.telemetry_endpoint_url = sdl2_config->Get(
"WebService", "telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry"); "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_username = sdl2_config->Get("WebService", "citra_username", "");
Settings::values.citra_token = sdl2_config->Get("WebService", "citra_token", ""); Settings::values.citra_token = sdl2_config->Get("WebService", "citra_token", "");
} }

View File

@ -185,6 +185,8 @@ gdbstub_port=24689
enable_telemetry = enable_telemetry =
# Endpoint URL for submitting telemetry data # Endpoint URL for submitting telemetry data
telemetry_endpoint_url = https://services.citra-emu.org/api/telemetry 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 # Username and token for Citra Web Service
# See https://services.citra-emu.org/ for more info # See https://services.citra-emu.org/ for more info
citra_username = citra_username =

View File

@ -79,6 +79,7 @@ set(UIS
main.ui main.ui
) )
file(GLOB_RECURSE ICONS ${CMAKE_SOURCE_DIR}/dist/icons/*)
file(GLOB_RECURSE THEMES ${CMAKE_SOURCE_DIR}/dist/qt_themes/*) file(GLOB_RECURSE THEMES ${CMAKE_SOURCE_DIR}/dist/qt_themes/*)
create_directory_groups(${SRCS} ${HEADERS} ${UIS}) create_directory_groups(${SRCS} ${HEADERS} ${UIS})
@ -92,10 +93,10 @@ endif()
if (APPLE) if (APPLE)
set(MACOSX_ICON "../../dist/citra.icns") set(MACOSX_ICON "../../dist/citra.icns")
set_source_files_properties(${MACOSX_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) 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) set_target_properties(citra-qt PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist)
else() else()
add_executable(citra-qt ${SRCS} ${HEADERS} ${UI_HDRS} ${THEMES}) add_executable(citra-qt ${SRCS} ${HEADERS} ${UI_HDRS} ${ICONS} ${THEMES})
endif() endif()
target_link_libraries(citra-qt PRIVATE audio_core common core input_common network video_core) 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) target_link_libraries(citra-qt PRIVATE Boost::boost glad nihstro-headers Qt5::OpenGL Qt5::Widgets)

View File

@ -146,6 +146,10 @@ void Config::ReadValues() {
qt_config->value("telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry") qt_config->value("telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry")
.toString() .toString()
.toStdString(); .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_username = qt_config->value("citra_username").toString().toStdString();
Settings::values.citra_token = qt_config->value("citra_token").toString().toStdString(); Settings::values.citra_token = qt_config->value("citra_token").toString().toStdString();
qt_config->endGroup(); qt_config->endGroup();
@ -293,6 +297,8 @@ void Config::SaveValues() {
qt_config->setValue("enable_telemetry", Settings::values.enable_telemetry); qt_config->setValue("enable_telemetry", Settings::values.enable_telemetry);
qt_config->setValue("telemetry_endpoint_url", qt_config->setValue("telemetry_endpoint_url",
QString::fromStdString(Settings::values.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_username", QString::fromStdString(Settings::values.citra_username));
qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token)); qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token));
qt_config->endGroup(); qt_config->endGroup();

View File

@ -2,6 +2,7 @@
// 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 <QMessageBox>
#include "citra_qt/configuration/configure_web.h" #include "citra_qt/configuration/configure_web.h"
#include "core/settings.h" #include "core/settings.h"
#include "core/telemetry_session.h" #include "core/telemetry_session.h"
@ -11,7 +12,9 @@ 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);
connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, 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(); this->setConfiguration();
} }
@ -34,19 +37,66 @@ void ConfigureWeb::setConfiguration() {
ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry); ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry);
ui->edit_username->setText(QString::fromStdString(Settings::values.citra_username)); ui->edit_username->setText(QString::fromStdString(Settings::values.citra_username));
ui->edit_token->setText(QString::fromStdString(Settings::values.citra_token)); 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" + ui->label_telemetry_id->setText("Telemetry ID: 0x" +
QString::number(Core::GetTelemetryId(), 16).toUpper()); QString::number(Core::GetTelemetryId(), 16).toUpper());
user_verified = true;
} }
void ConfigureWeb::applyConfiguration() { void ConfigureWeb::applyConfiguration() {
Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked(); Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked();
if (user_verified) {
Settings::values.citra_username = ui->edit_username->text().toStdString(); Settings::values.citra_username = ui->edit_username->text().toStdString();
Settings::values.citra_token = ui->edit_token->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(); Settings::Apply();
} }
void ConfigureWeb::refreshTelemetryID() { void ConfigureWeb::RefreshTelemetryID() {
const u64 new_telemetry_id{Core::RegenerateTelemetryId()}; const u64 new_telemetry_id{Core::RegenerateTelemetryId()};
ui->label_telemetry_id->setText("Telemetry ID: 0x" + ui->label_telemetry_id->setText("Telemetry ID: 0x" +
QString::number(new_telemetry_id, 16).toUpper()); 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."));
}
}

View File

@ -4,6 +4,7 @@
#pragma once #pragma once
#include <future>
#include <memory> #include <memory>
#include <QWidget> #include <QWidget>
@ -21,10 +22,19 @@ public:
void applyConfiguration(); void applyConfiguration();
public slots: public slots:
void refreshTelemetryID(); void RefreshTelemetryID();
void OnLoginChanged();
void VerifyLogin();
void OnLoginVerified();
signals:
void LoginVerified();
private: private:
void setConfiguration(); void setConfiguration();
bool user_verified = true;
std::future<bool> verified;
std::unique_ptr<Ui::ConfigureWeb> ui; std::unique_ptr<Ui::ConfigureWeb> ui;
}; };

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>400</width> <width>926</width>
<height>300</height> <height>561</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -31,14 +31,30 @@
</item> </item>
<item> <item>
<layout class="QGridLayout" name="gridLayoutCitraUsername"> <layout class="QGridLayout" name="gridLayoutCitraUsername">
<item row="0" column="0"> <item row="2" column="3">
<widget class="QLabel" name="label_username"> <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"> <property name="text">
<string>Username: </string> <string>Verify</string>
</property> </property>
</widget> </widget>
</item> </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"> <widget class="QLineEdit" name="edit_username">
<property name="maxLength"> <property name="maxLength">
<number>36</number> <number>36</number>
@ -52,7 +68,22 @@
</property> </property>
</widget> </widget>
</item> </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"> <widget class="QLineEdit" name="edit_token">
<property name="maxLength"> <property name="maxLength">
<number>36</number> <number>36</number>
@ -62,13 +93,6 @@
</property> </property>
</widget> </widget>
</item> </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"> <item row="2" column="1">
<widget class="QLabel" name="web_token_info_link"> <widget class="QLabel" name="web_token_info_link">
<property name="text"> <property name="text">
@ -76,6 +100,19 @@
</property> </property>
</widget> </widget>
</item> </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> </layout>
</item> </item>
</layout> </layout>

View File

@ -133,6 +133,7 @@ struct Values {
// WebService // WebService
bool enable_telemetry; bool enable_telemetry;
std::string telemetry_endpoint_url; std::string telemetry_endpoint_url;
std::string verify_endpoint_url;
std::string citra_username; std::string citra_username;
std::string citra_token; std::string citra_token;
} extern values; } extern values;

View File

@ -15,6 +15,7 @@
#ifdef ENABLE_WEB_SERVICE #ifdef ENABLE_WEB_SERVICE
#include "web_service/telemetry_json.h" #include "web_service/telemetry_json.h"
#include "web_service/verify_login.h"
#endif #endif
namespace Core { namespace Core {
@ -75,6 +76,17 @@ u64 RegenerateTelemetryId() {
return new_telemetry_id; 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() { TelemetrySession::TelemetrySession() {
#ifdef ENABLE_WEB_SERVICE #ifdef ENABLE_WEB_SERVICE
if (Settings::values.enable_telemetry) { if (Settings::values.enable_telemetry) {

View File

@ -4,6 +4,7 @@
#pragma once #pragma once
#include <future>
#include <memory> #include <memory>
#include "common/telemetry.h" #include "common/telemetry.h"
@ -47,4 +48,13 @@ u64 GetTelemetryId();
*/ */
u64 RegenerateTelemetryId(); 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 } // namespace Core

View File

@ -1,10 +1,12 @@
set(SRCS set(SRCS
telemetry_json.cpp telemetry_json.cpp
verify_login.cpp
web_backend.cpp web_backend.cpp
) )
set(HEADERS set(HEADERS
telemetry_json.h telemetry_json.h
verify_login.h
web_backend.h web_backend.h
) )

View File

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

View File

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

View File

@ -18,6 +18,19 @@ static constexpr char API_VERSION[]{"1"};
static std::unique_ptr<cpr::Session> g_session; 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, void PostJson(const std::string& url, const std::string& data, bool allow_anonymous,
const std::string& username, const std::string& token) { const std::string& username, const std::string& token) {
if (url.empty()) { if (url.empty()) {
@ -31,16 +44,7 @@ void PostJson(const std::string& url, const std::string& data, bool allow_anonym
return; return;
} }
#ifdef _WIN32 Win32WSAStartup();
// 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
// Built request header // Built request header
cpr::Header header; cpr::Header header;
@ -56,8 +60,81 @@ void PostJson(const std::string& url, const std::string& data, bool allow_anonym
} }
// Post JSON asynchronously // Post JSON asynchronously
static cpr::AsyncResponse future; static std::future<void> future;
future = cpr::PostAsync(cpr::Url{url.c_str()}, cpr::Body{data.c_str()}, header); 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 } // namespace WebService

View File

@ -4,6 +4,8 @@
#pragma once #pragma once
#include <functional>
#include <future>
#include <string> #include <string>
#include "common/common_types.h" #include "common/common_types.h"
@ -20,4 +22,18 @@ namespace WebService {
void PostJson(const std::string& url, const std::string& data, bool allow_anonymous, void PostJson(const std::string& url, const std::string& data, bool allow_anonymous,
const std::string& username = {}, const std::string& token = {}); 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 } // namespace WebService