web_backend: Make Client use the PImpl idiom

Like with TelemetryJson, we can make the implementation details private
and avoid the need to expose httplib to external libraries that need to
use the Client class.
pull/8/head
Lioncash 7 years ago
parent a7725d354c
commit 183a664405

@ -5,6 +5,7 @@
#pragma once #pragma once
#include <string> #include <string>
#include "common/common_types.h"
namespace Common { namespace Common {
struct WebResult { struct WebResult {

@ -4,6 +4,7 @@
#include <json.hpp> #include <json.hpp>
#include "common/detached_tasks.h" #include "common/detached_tasks.h"
#include "common/web_result.h"
#include "web_service/telemetry_json.h" #include "web_service/telemetry_json.h"
#include "web_service/web_backend.h" #include "web_service/web_backend.h"

@ -3,6 +3,7 @@
// Refer to the license.txt file included. // Refer to the license.txt file included.
#include <json.hpp> #include <json.hpp>
#include "common/web_result.h"
#include "web_service/verify_login.h" #include "web_service/verify_login.h"
#include "web_service/web_backend.h" #include "web_service/web_backend.h"

@ -3,9 +3,11 @@
// Refer to the license.txt file included. // Refer to the license.txt file included.
#include <cstdlib> #include <cstdlib>
#include <mutex>
#include <string> #include <string>
#include <thread>
#include <LUrlParser.h> #include <LUrlParser.h>
#include <httplib.h>
#include "common/common_types.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/web_result.h" #include "common/web_result.h"
#include "core/settings.h" #include "core/settings.h"
@ -20,99 +22,132 @@ constexpr u32 HTTPS_PORT = 443;
constexpr u32 TIMEOUT_SECONDS = 30; constexpr u32 TIMEOUT_SECONDS = 30;
Client::JWTCache Client::jwt_cache{}; struct Client::Impl {
Impl(std::string host, std::string username, std::string token)
: host{std::move(host)}, username{std::move(username)}, token{std::move(token)} {
std::lock_guard<std::mutex> lock(jwt_cache.mutex);
if (this->username == jwt_cache.username && this->token == jwt_cache.token) {
jwt = jwt_cache.jwt;
}
}
/// 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) {
if (jwt.empty()) {
UpdateJWT();
}
if (jwt.empty() && !allow_anonymous) {
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
return Common::WebResult{Common::WebResult::Code::CredentialsMissing,
"Credentials needed"};
}
auto result = GenericJson(method, path, data, jwt);
if (result.result_string == "401") {
// Try again with new JWT
UpdateJWT();
result = GenericJson(method, path, data, jwt);
}
Client::Client(const std::string& host, const std::string& username, const std::string& token) return result;
: host(host), username(username), token(token) {
std::lock_guard<std::mutex> lock(jwt_cache.mutex);
if (username == jwt_cache.username && token == jwt_cache.token) {
jwt = jwt_cache.jwt;
} }
}
Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, /**
const std::string& data, const std::string& jwt, * A generic function with explicit authentication method specified
const std::string& username, const std::string& token) { * JWT is used if the jwt parameter is not empty
if (cli == nullptr) { * username + token is used if jwt is empty but username and token are not empty
auto parsedUrl = LUrlParser::clParseURL::ParseURL(host); * anonymous if all of jwt, username and token are empty
int port; */
if (parsedUrl.m_Scheme == "http") { Common::WebResult GenericJson(const std::string& method, const std::string& path,
if (!parsedUrl.GetPort(&port)) { const std::string& data, const std::string& jwt = "",
port = HTTP_PORT; const std::string& username = "", const std::string& token = "") {
} if (cli == nullptr) {
cli = auto parsedUrl = LUrlParser::clParseURL::ParseURL(host);
std::make_unique<httplib::Client>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS); int port;
} else if (parsedUrl.m_Scheme == "https") { if (parsedUrl.m_Scheme == "http") {
if (!parsedUrl.GetPort(&port)) { if (!parsedUrl.GetPort(&port)) {
port = HTTPS_PORT; port = HTTP_PORT;
}
cli = std::make_unique<httplib::Client>(parsedUrl.m_Host.c_str(), port,
TIMEOUT_SECONDS);
} else if (parsedUrl.m_Scheme == "https") {
if (!parsedUrl.GetPort(&port)) {
port = HTTPS_PORT;
}
cli = std::make_unique<httplib::SSLClient>(parsedUrl.m_Host.c_str(), port,
TIMEOUT_SECONDS);
} else {
LOG_ERROR(WebService, "Bad URL scheme {}", parsedUrl.m_Scheme);
return Common::WebResult{Common::WebResult::Code::InvalidURL, "Bad URL scheme"};
} }
cli = std::make_unique<httplib::SSLClient>(parsedUrl.m_Host.c_str(), port,
TIMEOUT_SECONDS);
} else {
LOG_ERROR(WebService, "Bad URL scheme {}", parsedUrl.m_Scheme);
return Common::WebResult{Common::WebResult::Code::InvalidURL, "Bad URL scheme"};
} }
} if (cli == nullptr) {
if (cli == nullptr) { LOG_ERROR(WebService, "Invalid URL {}", host + path);
LOG_ERROR(WebService, "Invalid URL {}", host + path); return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"};
return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"}; }
}
httplib::Headers params; httplib::Headers params;
if (!jwt.empty()) { if (!jwt.empty()) {
params = { params = {
{std::string("Authorization"), fmt::format("Bearer {}", jwt)}, {std::string("Authorization"), fmt::format("Bearer {}", jwt)},
}; };
} else if (!username.empty()) { } else if (!username.empty()) {
params = { params = {
{std::string("x-username"), username}, {std::string("x-username"), username},
{std::string("x-token"), token}, {std::string("x-token"), token},
};
}
params.emplace(std::string("api-version"),
std::string(API_VERSION.begin(), API_VERSION.end()));
if (method != "GET") {
params.emplace(std::string("Content-Type"), std::string("application/json"));
}; };
}
params.emplace(std::string("api-version"), std::string(API_VERSION.begin(), API_VERSION.end())); httplib::Request request;
if (method != "GET") { request.method = method;
params.emplace(std::string("Content-Type"), std::string("application/json")); request.path = path;
}; request.headers = params;
request.body = data;
httplib::Request request; httplib::Response response;
request.method = method;
request.path = path;
request.headers = params;
request.body = data;
httplib::Response response; if (!cli->send(request, response)) {
LOG_ERROR(WebService, "{} to {} returned null", method, host + path);
return Common::WebResult{Common::WebResult::Code::LibError, "Null response"};
}
if (!cli->send(request, response)) { if (response.status >= 400) {
LOG_ERROR(WebService, "{} to {} returned null", method, host + path); LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path,
return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; response.status);
} return Common::WebResult{Common::WebResult::Code::HttpError,
std::to_string(response.status)};
}
if (response.status >= 400) { auto content_type = response.headers.find("content-type");
LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path,
response.status);
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()) {
LOG_ERROR(WebService, "{} to {} returned no content", method, host + path);
return Common::WebResult{Common::WebResult::Code::WrongContent, ""};
}
if (content_type == response.headers.end()) { if (content_type->second.find("application/json") == std::string::npos &&
LOG_ERROR(WebService, "{} to {} returned no content", method, host + path); content_type->second.find("text/html; charset=utf-8") == std::string::npos) {
return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path,
content_type->second);
return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"};
}
return Common::WebResult{Common::WebResult::Code::Success, "", response.body};
} }
if (content_type->second.find("application/json") == std::string::npos && // Retrieve a new JWT from given username and token
content_type->second.find("text/html; charset=utf-8") == std::string::npos) { void UpdateJWT() {
LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, if (username.empty() || token.empty()) {
content_type->second); return;
return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"}; }
}
return Common::WebResult{Common::WebResult::Code::Success, "", response.body};
}
void Client::UpdateJWT() {
if (!username.empty() && !token.empty()) {
auto result = GenericJson("POST", "/jwt/internal", "", "", username, token); auto result = GenericJson("POST", "/jwt/internal", "", "", 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");
@ -123,27 +158,39 @@ void Client::UpdateJWT() {
jwt_cache.jwt = jwt = result.returned_data; jwt_cache.jwt = jwt = result.returned_data;
} }
} }
}
Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, std::string host;
const std::string& data, bool allow_anonymous) { std::string username;
if (jwt.empty()) { std::string token;
UpdateJWT(); std::string jwt;
} std::unique_ptr<httplib::Client> cli;
struct JWTCache {
std::mutex mutex;
std::string username;
std::string token;
std::string jwt;
};
static inline JWTCache jwt_cache;
};
if (jwt.empty() && !allow_anonymous) { Client::Client(std::string host, std::string username, std::string token)
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); : impl{std::make_unique<Impl>(std::move(host), std::move(username), std::move(token))} {}
return Common::WebResult{Common::WebResult::Code::CredentialsMissing, "Credentials needed"};
}
auto result = GenericJson(method, path, data, jwt); Client::~Client() = default;
if (result.result_string == "401") {
// Try again with new JWT Common::WebResult Client::PostJson(const std::string& path, const std::string& data,
UpdateJWT(); bool allow_anonymous) {
result = GenericJson(method, path, data, jwt); return impl->GenericJson("POST", path, data, allow_anonymous);
} }
Common::WebResult Client::GetJson(const std::string& path, bool allow_anonymous) {
return impl->GenericJson("GET", path, "", allow_anonymous);
}
return result; Common::WebResult Client::DeleteJson(const std::string& path, const std::string& data,
bool allow_anonymous) {
return impl->GenericJson("DELETE", path, data, allow_anonymous);
} }
} // namespace WebService } // namespace WebService

@ -4,23 +4,19 @@
#pragma once #pragma once
#include <functional> #include <memory>
#include <mutex>
#include <string> #include <string>
#include <tuple>
#include <httplib.h>
#include "common/common_types.h"
#include "common/web_result.h"
namespace httplib { namespace Common {
class Client; struct WebResult;
} }
namespace WebService { namespace WebService {
class Client { class Client {
public: public:
Client(const std::string& host, const std::string& username, const std::string& token); Client(std::string host, std::string username, std::string token);
~Client();
/** /**
* Posts JSON to the specified path. * Posts JSON to the specified path.
@ -30,9 +26,7 @@ public:
* @return the result of the request. * @return the result of the request.
*/ */
Common::WebResult PostJson(const std::string& path, const std::string& data, Common::WebResult PostJson(const std::string& path, const std::string& data,
bool allow_anonymous) { bool allow_anonymous);
return GenericJson("POST", path, data, allow_anonymous);
}
/** /**
* Gets JSON from the specified path. * Gets JSON from the specified path.
@ -40,9 +34,7 @@ public:
* @param allow_anonymous If true, allow anonymous unauthenticated requests. * @param allow_anonymous If true, allow anonymous unauthenticated requests.
* @return the result of the request. * @return the result of the request.
*/ */
Common::WebResult GetJson(const std::string& path, bool allow_anonymous) { Common::WebResult GetJson(const std::string& path, bool allow_anonymous);
return GenericJson("GET", path, "", allow_anonymous);
}
/** /**
* Deletes JSON to the specified path. * Deletes JSON to the specified path.
@ -52,41 +44,11 @@ public:
* @return the result of the request. * @return the result of the request.
*/ */
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);
return GenericJson("DELETE", path, data, allow_anonymous);
}
private: private:
/// A generic function handles POST, GET and DELETE request together struct Impl;
Common::WebResult GenericJson(const std::string& method, const std::string& path, std::unique_ptr<Impl> impl;
const std::string& data, bool allow_anonymous);
/**
* 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
*/
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 = "");
// Retrieve a new JWT from given username and token
void UpdateJWT();
std::string host;
std::string username;
std::string token;
std::string jwt;
std::unique_ptr<httplib::Client> cli;
struct JWTCache {
std::mutex mutex;
std::string username;
std::string token;
std::string jwt;
};
static JWTCache jwt_cache;
}; };
} // namespace WebService } // namespace WebService

Loading…
Cancel
Save