2017-06-28 05:01:49 +02:00
|
|
|
// Copyright 2017 Citra Emulator Project
|
|
|
|
// Licensed under GPLv2 or any later version
|
|
|
|
// Refer to the license.txt file included.
|
|
|
|
|
2017-08-25 01:27:13 +02:00
|
|
|
#include <cstdlib>
|
2018-03-24 20:19:35 +01:00
|
|
|
#include <string>
|
2017-08-25 01:27:13 +02:00
|
|
|
#include <thread>
|
2018-03-24 20:19:35 +01:00
|
|
|
#include <LUrlParser.h>
|
2017-10-31 10:02:42 +01:00
|
|
|
#include "common/announce_multiplayer_room.h"
|
2017-06-28 05:18:52 +02:00
|
|
|
#include "common/logging/log.h"
|
2018-08-25 21:39:23 +02:00
|
|
|
#include "core/settings.h"
|
2017-06-28 05:01:49 +02:00
|
|
|
#include "web_service/web_backend.h"
|
|
|
|
|
|
|
|
namespace WebService {
|
|
|
|
|
2017-07-10 00:37:14 +02:00
|
|
|
static constexpr char API_VERSION[]{"1"};
|
2017-06-28 05:18:52 +02:00
|
|
|
|
2018-03-24 20:19:35 +01:00
|
|
|
constexpr int HTTP_PORT = 80;
|
|
|
|
constexpr int HTTPS_PORT = 443;
|
|
|
|
|
|
|
|
constexpr int TIMEOUT_SECONDS = 30;
|
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
std::string UpdateCoreJWT(bool force_new_token, const std::string& username,
|
|
|
|
const std::string& token) {
|
|
|
|
static std::string jwt;
|
|
|
|
if (jwt.empty() || force_new_token) {
|
|
|
|
if (!username.empty() && !token.empty()) {
|
2018-08-27 23:09:29 +02:00
|
|
|
std::future<Common::WebResult> future = PostJson(
|
|
|
|
Settings::values.web_services_endpoint_url + "/jwt/internal", username, token);
|
2018-08-25 21:39:23 +02:00
|
|
|
jwt = future.get().returned_data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return jwt;
|
|
|
|
}
|
|
|
|
|
2018-03-24 20:19:35 +01:00
|
|
|
std::unique_ptr<httplib::Client> GetClientFor(const LUrlParser::clParseURL& parsedUrl) {
|
|
|
|
namespace hl = httplib;
|
|
|
|
|
|
|
|
int port;
|
|
|
|
|
|
|
|
std::unique_ptr<hl::Client> cli;
|
|
|
|
|
|
|
|
if (parsedUrl.m_Scheme == "http") {
|
|
|
|
if (!parsedUrl.GetPort(&port)) {
|
|
|
|
port = HTTP_PORT;
|
|
|
|
}
|
2018-06-14 08:00:07 +02:00
|
|
|
return std::make_unique<hl::Client>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS);
|
2018-03-24 20:19:35 +01:00
|
|
|
} else if (parsedUrl.m_Scheme == "https") {
|
|
|
|
if (!parsedUrl.GetPort(&port)) {
|
|
|
|
port = HTTPS_PORT;
|
|
|
|
}
|
2018-06-14 08:00:07 +02:00
|
|
|
return std::make_unique<hl::SSLClient>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS);
|
2018-03-24 20:19:35 +01:00
|
|
|
} else {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "Bad URL scheme {}", parsedUrl.m_Scheme);
|
2018-03-24 20:19:35 +01:00
|
|
|
return nullptr;
|
2017-09-19 03:18:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
static Common::WebResult PostJsonAsyncFn(const std::string& url,
|
|
|
|
const LUrlParser::clParseURL& parsed_url,
|
|
|
|
const httplib::Headers& params, const std::string& data,
|
|
|
|
bool is_jwt_requested) {
|
|
|
|
static bool is_first_attempt = true;
|
|
|
|
|
|
|
|
namespace hl = httplib;
|
|
|
|
std::unique_ptr<hl::Client> cli = GetClientFor(parsed_url);
|
|
|
|
|
|
|
|
if (cli == nullptr) {
|
|
|
|
return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"};
|
|
|
|
}
|
|
|
|
|
|
|
|
hl::Request request;
|
|
|
|
request.method = "POST";
|
|
|
|
request.path = "/" + parsed_url.m_Path;
|
|
|
|
request.headers = params;
|
|
|
|
request.body = data;
|
|
|
|
|
|
|
|
hl::Response response;
|
|
|
|
|
|
|
|
if (!cli->send(request, response)) {
|
|
|
|
LOG_ERROR(WebService, "POST to {} returned null", url);
|
|
|
|
return Common::WebResult{Common::WebResult::Code::LibError, "Null response"};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (response.status >= 400) {
|
|
|
|
LOG_ERROR(WebService, "POST to {} returned error status code: {}", url, response.status);
|
|
|
|
if (response.status == 401 && !is_jwt_requested && is_first_attempt) {
|
|
|
|
LOG_WARNING(WebService, "Requesting new JWT");
|
|
|
|
UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token);
|
|
|
|
is_first_attempt = false;
|
|
|
|
PostJsonAsyncFn(url, parsed_url, params, data, is_jwt_requested);
|
|
|
|
is_first_attempt = true;
|
|
|
|
}
|
|
|
|
return Common::WebResult{Common::WebResult::Code::HttpError,
|
|
|
|
std::to_string(response.status)};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto content_type = response.headers.find("content-type");
|
|
|
|
|
|
|
|
if (content_type == response.headers.end() ||
|
|
|
|
(content_type->second.find("application/json") == std::string::npos &&
|
|
|
|
content_type->second.find("text/html; charset=utf-8") == std::string::npos)) {
|
|
|
|
LOG_ERROR(WebService, "POST to {} returned wrong content: {}", url, content_type->second);
|
|
|
|
return Common::WebResult{Common::WebResult::Code::WrongContent, ""};
|
|
|
|
}
|
|
|
|
|
|
|
|
return Common::WebResult{Common::WebResult::Code::Success, "", response.body};
|
|
|
|
}
|
|
|
|
|
2017-10-31 10:02:42 +01:00
|
|
|
std::future<Common::WebResult> PostJson(const std::string& url, const std::string& data,
|
2018-08-25 21:39:23 +02:00
|
|
|
bool allow_anonymous) {
|
|
|
|
|
2018-03-24 20:19:35 +01:00
|
|
|
using lup = LUrlParser::clParseURL;
|
|
|
|
namespace hl = httplib;
|
|
|
|
|
|
|
|
lup parsedUrl = lup::ParseURL(url);
|
|
|
|
|
|
|
|
if (url.empty() || !parsedUrl.IsValid()) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "URL is invalid");
|
2018-08-25 21:39:23 +02:00
|
|
|
return std::async(std::launch::deferred, [] {
|
2017-10-31 10:02:42 +01:00
|
|
|
return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"};
|
|
|
|
});
|
2017-06-28 05:18:52 +02:00
|
|
|
}
|
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
const std::string jwt =
|
|
|
|
UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token);
|
|
|
|
|
|
|
|
const bool are_credentials_provided{!jwt.empty()};
|
2017-08-24 03:09:34 +02:00
|
|
|
if (!allow_anonymous && !are_credentials_provided) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
|
2018-08-25 21:39:23 +02:00
|
|
|
return std::async(std::launch::deferred, [] {
|
2017-10-31 10:02:42 +01:00
|
|
|
return Common::WebResult{Common::WebResult::Code::CredentialsMissing,
|
|
|
|
"Credentials needed"};
|
|
|
|
});
|
2017-06-28 05:18:52 +02:00
|
|
|
}
|
|
|
|
|
2017-08-27 01:02:03 +02:00
|
|
|
// Built request header
|
2018-03-24 20:19:35 +01:00
|
|
|
hl::Headers params;
|
2017-08-24 03:09:34 +02:00
|
|
|
if (are_credentials_provided) {
|
|
|
|
// Authenticated request if credentials are provided
|
2018-08-25 21:39:23 +02:00
|
|
|
params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)},
|
2018-03-24 20:19:35 +01:00
|
|
|
{std::string("api-version"), std::string(API_VERSION)},
|
|
|
|
{std::string("Content-Type"), std::string("application/json")}};
|
2017-08-24 03:09:34 +02:00
|
|
|
} else {
|
|
|
|
// Otherwise, anonymous request
|
2018-03-24 20:19:35 +01:00
|
|
|
params = {{std::string("api-version"), std::string(API_VERSION)},
|
|
|
|
{std::string("Content-Type"), std::string("application/json")}};
|
2017-06-28 05:18:52 +02:00
|
|
|
}
|
2017-08-27 01:02:03 +02:00
|
|
|
|
|
|
|
// Post JSON asynchronously
|
2018-08-25 21:39:23 +02:00
|
|
|
return std::async(std::launch::async, PostJsonAsyncFn, url, parsedUrl, params, data, false);
|
|
|
|
}
|
2018-03-24 20:19:35 +01:00
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
std::future<Common::WebResult> PostJson(const std::string& url, const std::string& username,
|
|
|
|
const std::string& token) {
|
|
|
|
using lup = LUrlParser::clParseURL;
|
|
|
|
namespace hl = httplib;
|
2018-03-24 20:19:35 +01:00
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
lup parsedUrl = lup::ParseURL(url);
|
2018-03-24 20:19:35 +01:00
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
if (url.empty() || !parsedUrl.IsValid()) {
|
|
|
|
LOG_ERROR(WebService, "URL is invalid");
|
|
|
|
return std::async(std::launch::deferred, [] {
|
|
|
|
return Common::WebResult{Common::WebResult::Code::InvalidURL, ""};
|
|
|
|
});
|
|
|
|
}
|
2018-03-24 20:19:35 +01:00
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
const bool are_credentials_provided{!token.empty() && !username.empty()};
|
|
|
|
if (!are_credentials_provided) {
|
|
|
|
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
|
|
|
|
return std::async(std::launch::deferred, [] {
|
|
|
|
return Common::WebResult{Common::WebResult::Code::CredentialsMissing, ""};
|
|
|
|
});
|
|
|
|
}
|
2018-03-24 20:19:35 +01:00
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
// Built request header
|
|
|
|
hl::Headers params;
|
|
|
|
if (are_credentials_provided) {
|
|
|
|
// Authenticated request if credentials are provided
|
|
|
|
params = {{std::string("x-username"), username},
|
|
|
|
{std::string("x-token"), token},
|
|
|
|
{std::string("api-version"), std::string(API_VERSION)},
|
|
|
|
{std::string("Content-Type"), std::string("application/json")}};
|
|
|
|
} else {
|
|
|
|
// Otherwise, anonymous request
|
|
|
|
params = {{std::string("api-version"), std::string(API_VERSION)},
|
|
|
|
{std::string("Content-Type"), std::string("application/json")}};
|
|
|
|
}
|
2018-03-24 20:19:35 +01:00
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
// Post JSON asynchronously
|
|
|
|
return std::async(std::launch::async, PostJsonAsyncFn, url, parsedUrl, params, "", true);
|
2017-09-19 03:18:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
std::future<T> GetJson(std::function<T(const std::string&)> func, const std::string& url,
|
2018-08-25 21:39:23 +02:00
|
|
|
bool allow_anonymous) {
|
|
|
|
static bool is_first_attempt = true;
|
|
|
|
|
2018-03-24 20:19:35 +01:00
|
|
|
using lup = LUrlParser::clParseURL;
|
|
|
|
namespace hl = httplib;
|
|
|
|
|
|
|
|
lup parsedUrl = lup::ParseURL(url);
|
|
|
|
|
|
|
|
if (url.empty() || !parsedUrl.IsValid()) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "URL is invalid");
|
2017-11-07 21:51:11 +01:00
|
|
|
return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); });
|
2017-09-19 03:18:26 +02:00
|
|
|
}
|
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
const std::string jwt =
|
|
|
|
UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token);
|
|
|
|
|
|
|
|
const bool are_credentials_provided{!jwt.empty()};
|
2017-09-19 03:18:26 +02:00
|
|
|
if (!allow_anonymous && !are_credentials_provided) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
|
2017-11-07 21:51:11 +01:00
|
|
|
return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); });
|
2017-09-19 03:18:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Built request header
|
2018-03-24 20:19:35 +01:00
|
|
|
hl::Headers params;
|
2017-09-19 03:18:26 +02:00
|
|
|
if (are_credentials_provided) {
|
2018-08-25 21:39:23 +02:00
|
|
|
params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)},
|
2018-03-24 20:19:35 +01:00
|
|
|
{std::string("api-version"), std::string(API_VERSION)}};
|
2017-09-19 03:18:26 +02:00
|
|
|
} else {
|
|
|
|
// Otherwise, anonymous request
|
2018-03-24 20:19:35 +01:00
|
|
|
params = {{std::string("api-version"), std::string(API_VERSION)}};
|
2017-09-19 03:18:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get JSON asynchronously
|
2018-08-25 21:39:23 +02:00
|
|
|
return std::async(std::launch::async, [func, url, parsedUrl, params, allow_anonymous] {
|
2018-03-24 20:19:35 +01:00
|
|
|
std::unique_ptr<hl::Client> cli = GetClientFor(parsedUrl);
|
|
|
|
|
|
|
|
if (cli == nullptr) {
|
|
|
|
return func("");
|
|
|
|
}
|
|
|
|
|
|
|
|
hl::Request request;
|
|
|
|
request.method = "GET";
|
|
|
|
request.path = "/" + parsedUrl.m_Path;
|
|
|
|
request.headers = params;
|
|
|
|
|
|
|
|
hl::Response response;
|
|
|
|
|
|
|
|
if (!cli->send(request, response)) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "GET to {} returned null", url);
|
2018-03-24 20:19:35 +01:00
|
|
|
return func("");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (response.status >= 400) {
|
2018-06-29 15:56:12 +02:00
|
|
|
LOG_ERROR(WebService, "GET to {} returned error status code: {}", url, response.status);
|
2018-08-25 21:39:23 +02:00
|
|
|
if (response.status == 401 && is_first_attempt) {
|
|
|
|
LOG_WARNING(WebService, "Requesting new JWT");
|
|
|
|
UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token);
|
|
|
|
is_first_attempt = false;
|
|
|
|
GetJson(func, url, allow_anonymous);
|
|
|
|
is_first_attempt = true;
|
|
|
|
}
|
2018-03-24 20:19:35 +01:00
|
|
|
return func("");
|
|
|
|
}
|
|
|
|
|
|
|
|
auto content_type = response.headers.find("content-type");
|
|
|
|
|
|
|
|
if (content_type == response.headers.end() ||
|
|
|
|
content_type->second.find("application/json") == std::string::npos) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "GET to {} returned wrong content: {}", url,
|
2018-06-29 15:56:12 +02:00
|
|
|
content_type->second);
|
2018-03-24 20:19:35 +01:00
|
|
|
return func("");
|
|
|
|
}
|
|
|
|
|
|
|
|
return func(response.body);
|
|
|
|
});
|
2017-06-28 05:18:52 +02:00
|
|
|
}
|
|
|
|
|
2017-09-19 03:18:26 +02:00
|
|
|
template std::future<bool> GetJson(std::function<bool(const std::string&)> func,
|
2018-08-25 21:39:23 +02:00
|
|
|
const std::string& url, bool allow_anonymous);
|
2017-10-31 10:02:42 +01:00
|
|
|
template std::future<AnnounceMultiplayerRoom::RoomList> GetJson(
|
|
|
|
std::function<AnnounceMultiplayerRoom::RoomList(const std::string&)> func,
|
2018-08-25 21:39:23 +02:00
|
|
|
const std::string& url, bool allow_anonymous);
|
|
|
|
|
|
|
|
void DeleteJson(const std::string& url, const std::string& data) {
|
|
|
|
static bool is_first_attempt = true;
|
2017-10-31 10:02:42 +01:00
|
|
|
|
2018-03-24 20:19:35 +01:00
|
|
|
using lup = LUrlParser::clParseURL;
|
|
|
|
namespace hl = httplib;
|
|
|
|
|
|
|
|
lup parsedUrl = lup::ParseURL(url);
|
|
|
|
|
|
|
|
if (url.empty() || !parsedUrl.IsValid()) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "URL is invalid");
|
2017-10-31 10:02:42 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-25 21:39:23 +02:00
|
|
|
const std::string jwt =
|
|
|
|
UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token);
|
|
|
|
|
|
|
|
const bool are_credentials_provided{!jwt.empty()};
|
2018-03-24 20:19:35 +01:00
|
|
|
if (!are_credentials_provided) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
|
2017-10-31 10:02:42 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Built request header
|
2018-08-25 21:39:23 +02:00
|
|
|
hl::Headers params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)},
|
2018-03-24 20:19:35 +01:00
|
|
|
{std::string("api-version"), std::string(API_VERSION)},
|
|
|
|
{std::string("Content-Type"), std::string("application/json")}};
|
2017-10-31 10:02:42 +01:00
|
|
|
|
|
|
|
// Delete JSON asynchronously
|
2018-03-24 20:19:35 +01:00
|
|
|
std::async(std::launch::async, [url, parsedUrl, params, data] {
|
|
|
|
std::unique_ptr<hl::Client> cli = GetClientFor(parsedUrl);
|
|
|
|
|
|
|
|
if (cli == nullptr) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
hl::Request request;
|
|
|
|
request.method = "DELETE";
|
|
|
|
request.path = "/" + parsedUrl.m_Path;
|
|
|
|
request.headers = params;
|
|
|
|
request.body = data;
|
|
|
|
|
|
|
|
hl::Response response;
|
|
|
|
|
|
|
|
if (!cli->send(request, response)) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "DELETE to {} returned null", url);
|
2018-03-24 20:19:35 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (response.status >= 400) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "DELETE to {} returned error status code: {}", url,
|
2018-06-29 15:56:12 +02:00
|
|
|
response.status);
|
2018-08-25 21:39:23 +02:00
|
|
|
if (response.status == 401 && is_first_attempt) {
|
|
|
|
LOG_WARNING(WebService, "Requesting new JWT");
|
|
|
|
UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token);
|
|
|
|
is_first_attempt = false;
|
|
|
|
DeleteJson(url, data);
|
|
|
|
is_first_attempt = true;
|
|
|
|
}
|
2018-03-24 20:19:35 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto content_type = response.headers.find("content-type");
|
|
|
|
|
|
|
|
if (content_type == response.headers.end() ||
|
|
|
|
content_type->second.find("application/json") == std::string::npos) {
|
2018-06-29 13:18:07 +02:00
|
|
|
LOG_ERROR(WebService, "DELETE to {} returned wrong content: {}", url,
|
2018-06-29 15:56:12 +02:00
|
|
|
content_type->second);
|
2018-03-24 20:19:35 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
});
|
2017-10-31 10:02:42 +01:00
|
|
|
}
|
2017-09-19 03:18:26 +02:00
|
|
|
|
2017-06-28 05:01:49 +02:00
|
|
|
} // namespace WebService
|