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.
This commit is contained in:
Lioncash 2018-10-10 21:23:41 -04:00
parent a7725d354c
commit 183a664405
5 changed files with 164 additions and 152 deletions

View file

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

View file

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

View file

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

View file

@ -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)
Client::Client(const std::string& host, const std::string& username, const std::string& token) : host{std::move(host)}, username{std::move(username)}, token{std::move(token)} {
: host(host), username(username), token(token) { std::lock_guard<std::mutex> lock(jwt_cache.mutex);
std::lock_guard<std::mutex> lock(jwt_cache.mutex); if (this->username == jwt_cache.username && this->token == jwt_cache.token) {
if (username == jwt_cache.username && token == jwt_cache.token) { jwt = jwt_cache.jwt;
jwt = jwt_cache.jwt;
}
}
Common::WebResult Client::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) {
if (cli == nullptr) {
auto parsedUrl = LUrlParser::clParseURL::ParseURL(host);
int port;
if (parsedUrl.m_Scheme == "http") {
if (!parsedUrl.GetPort(&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"};
} }
} }
if (cli == nullptr) {
LOG_ERROR(WebService, "Invalid URL {}", host + path); /// A generic function handles POST, GET and DELETE request together
return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"}; 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);
}
return result;
} }
httplib::Headers params; /**
if (!jwt.empty()) { * A generic function with explicit authentication method specified
params = { * JWT is used if the jwt parameter is not empty
{std::string("Authorization"), fmt::format("Bearer {}", jwt)}, * 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
} else if (!username.empty()) { */
params = { Common::WebResult GenericJson(const std::string& method, const std::string& path,
{std::string("x-username"), username}, const std::string& data, const std::string& jwt = "",
{std::string("x-token"), token}, const std::string& username = "", const std::string& token = "") {
if (cli == nullptr) {
auto parsedUrl = LUrlParser::clParseURL::ParseURL(host);
int port;
if (parsedUrl.m_Scheme == "http") {
if (!parsedUrl.GetPort(&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"};
}
}
if (cli == nullptr) {
LOG_ERROR(WebService, "Invalid URL {}", host + path);
return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"};
}
httplib::Headers params;
if (!jwt.empty()) {
params = {
{std::string("Authorization"), fmt::format("Bearer {}", jwt)},
};
} else if (!username.empty()) {
params = {
{std::string("x-username"), username},
{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"));
}; };
httplib::Request request;
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 (response.status >= 400) {
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->second.find("application/json") == 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,
content_type->second);
return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"};
}
return Common::WebResult{Common::WebResult::Code::Success, "", response.body};
} }
params.emplace(std::string("api-version"), std::string(API_VERSION.begin(), API_VERSION.end())); // Retrieve a new JWT from given username and token
if (method != "GET") { void UpdateJWT() {
params.emplace(std::string("Content-Type"), std::string("application/json")); if (username.empty() || token.empty()) {
}; return;
}
httplib::Request request;
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 (response.status >= 400) {
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->second.find("application/json") == 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,
content_type->second);
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;
} }
} }
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 inline JWTCache jwt_cache;
};
Client::Client(std::string host, std::string username, std::string token)
: impl{std::make_unique<Impl>(std::move(host), std::move(username), std::move(token))} {}
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);
} }
Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, Common::WebResult Client::GetJson(const std::string& path, bool allow_anonymous) {
const std::string& data, bool allow_anonymous) { return impl->GenericJson("GET", path, "", allow_anonymous);
if (jwt.empty()) { }
UpdateJWT();
}
if (jwt.empty() && !allow_anonymous) { Common::WebResult Client::DeleteJson(const std::string& path, const std::string& data,
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); bool allow_anonymous) {
return Common::WebResult{Common::WebResult::Code::CredentialsMissing, "Credentials needed"}; return impl->GenericJson("DELETE", path, data, allow_anonymous);
}
auto result = GenericJson(method, path, data, jwt);
if (result.result_string == "401") {
// Try again with new JWT
UpdateJWT();
result = GenericJson(method, path, data, jwt);
}
return result;
} }
} // namespace WebService } // namespace WebService

View file

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