Compare commits

...

3 Commits

Author SHA1 Message Date
Alex Kremer
a316486741 feat: Add v3 support (#1754) 2024-11-27 18:29:57 +00:00
Alex Kremer
e2caf577e0 feat: Healthcheck endpoint (#1751)
Fixes #1759
2024-11-27 18:29:32 +00:00
Alex Kremer
b23693e403 feat: Upgrade to libxrpl 2.3.0 (#1756) 2024-11-27 18:24:54 +00:00
7 changed files with 155 additions and 70 deletions

View File

@@ -28,7 +28,7 @@ class Clio(ConanFile):
'protobuf/3.21.9', 'protobuf/3.21.9',
'grpc/1.50.1', 'grpc/1.50.1',
'openssl/1.1.1u', 'openssl/1.1.1u',
'xrpl/2.3.0-rc2', 'xrpl/2.3.0',
'zlib/1.3.1', 'zlib/1.3.1',
'libbacktrace/cci.20210118' 'libbacktrace/cci.20210118'
] ]

View File

@@ -34,16 +34,13 @@ static constexpr uint32_t API_VERSION_DEFAULT = 1u;
/** /**
* @brief Minimum API version supported by this build * @brief Minimum API version supported by this build
*
* Note: Clio does not natively support v1 and only supports v2 or newer.
* However, Clio will forward all v1 requests to rippled for backward compatibility.
*/ */
static constexpr uint32_t API_VERSION_MIN = 1u; static constexpr uint32_t API_VERSION_MIN = 1u;
/** /**
* @brief Maximum API version supported by this build * @brief Maximum API version supported by this build
*/ */
static constexpr uint32_t API_VERSION_MAX = 2u; static constexpr uint32_t API_VERSION_MAX = 3u;
/** /**
* @brief A baseclass for API version helper * @brief A baseclass for API version helper

View File

@@ -60,6 +60,14 @@
namespace web::impl { namespace web::impl {
static auto constexpr HealthCheckHTML = R"html(
<!DOCTYPE html>
<html>
<head><title>Test page for Clio</title></head>
<body><h1>Clio Test</h1><p>This page shows Clio http(s) connectivity is working.</p></body>
</html>
)html";
using tcp = boost::asio::ip::tcp; using tcp = boost::asio::ip::tcp;
/** /**
@@ -205,6 +213,9 @@ public:
if (ec) if (ec)
return httpFail(ec, "read"); return httpFail(ec, "read");
if (req_.method() == http::verb::get and req_.target() == "/health")
return sender_(httpResponse(http::status::ok, "text/html", HealthCheckHTML));
// Update isAdmin property of the connection // Update isAdmin property of the connection
ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp); ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp);

View File

@@ -36,6 +36,7 @@
#include <boost/beast/http.hpp> // IWYU pragma: keep #include <boost/beast/http.hpp> // IWYU pragma: keep
#include <boost/beast/http/field.hpp> #include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp> #include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp> #include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp> #include <boost/beast/http/verb.hpp>
#include <boost/beast/http/write.hpp> // IWYU pragma: keep #include <boost/beast/http/write.hpp> // IWYU pragma: keep
@@ -58,7 +59,7 @@ using tcp = boost::asio::ip::tcp;
namespace { namespace {
std::string std::pair<boost::beast::http::status, std::string>
syncRequest( syncRequest(
std::string const& host, std::string const& host,
std::string const& port, std::string const& port,
@@ -96,7 +97,7 @@ syncRequest(
boost::beast::error_code ec; boost::beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec); stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return res.body(); return {res.result(), res.body()};
} }
} // namespace } // namespace
@@ -105,7 +106,7 @@ WebHeader::WebHeader(http::field name, std::string value) : name(name), value(st
{ {
} }
std::string std::pair<boost::beast::http::status, std::string>
HttpSyncClient::post( HttpSyncClient::post(
std::string const& host, std::string const& host,
std::string const& port, std::string const& port,
@@ -116,7 +117,7 @@ HttpSyncClient::post(
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post); return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post);
} }
std::string std::pair<boost::beast::http::status, std::string>
HttpSyncClient::get( HttpSyncClient::get(
std::string const& host, std::string const& host,
std::string const& port, std::string const& port,

View File

@@ -26,6 +26,7 @@
#include <boost/beast/core/tcp_stream.hpp> #include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http/field.hpp> #include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp> #include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp> #include <boost/beast/http/string_body.hpp>
#include <chrono> #include <chrono>
@@ -33,6 +34,7 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <utility>
#include <vector> #include <vector>
struct WebHeader { struct WebHeader {
@@ -43,7 +45,7 @@ struct WebHeader {
}; };
struct HttpSyncClient { struct HttpSyncClient {
static std::string static std::pair<boost::beast::http::status, std::string>
post( post(
std::string const& host, std::string const& host,
std::string const& port, std::string const& port,
@@ -51,7 +53,7 @@ struct HttpSyncClient {
std::vector<WebHeader> additionalHeaders = {} std::vector<WebHeader> additionalHeaders = {}
); );
static std::string static std::pair<boost::beast::http::status, std::string>
get(std::string const& host, get(std::string const& host,
std::string const& port, std::string const& port,
std::string const& body, std::string const& body,

View File

@@ -17,12 +17,14 @@
//============================================================================== //==============================================================================
#include "rpc/Errors.hpp" #include "rpc/Errors.hpp"
#include "rpc/common/APIVersion.hpp"
#include "rpc/common/Types.hpp" #include "rpc/common/Types.hpp"
#include "util/AsioContextTestFixture.hpp" #include "util/AsioContextTestFixture.hpp"
#include "util/MockBackendTestFixture.hpp" #include "util/MockBackendTestFixture.hpp"
#include "util/MockETLService.hpp" #include "util/MockETLService.hpp"
#include "util/MockPrometheus.hpp" #include "util/MockPrometheus.hpp"
#include "util/MockRPCEngine.hpp" #include "util/MockRPCEngine.hpp"
#include "util/NameGenerator.hpp"
#include "util/Taggable.hpp" #include "util/Taggable.hpp"
#include "util/config/Config.hpp" #include "util/config/Config.hpp"
#include "web/RPCServerHandler.hpp" #include "web/RPCServerHandler.hpp"
@@ -30,17 +32,22 @@
#include <boost/beast/http/status.hpp> #include <boost/beast/http/status.hpp>
#include <boost/json/parse.hpp> #include <boost/json/parse.hpp>
#include <fmt/core.h>
#include <gmock/gmock.h> #include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <cstdint>
#include <memory> #include <memory>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <vector>
using namespace web; using namespace web;
constexpr static auto MINSEQ = 10; namespace {
constexpr static auto MAXSEQ = 30;
constexpr auto MINSEQ = 10;
constexpr auto MAXSEQ = 30;
struct MockWsBase : public web::ConnectionBase { struct MockWsBase : public web::ConnectionBase {
std::string message; std::string message;
@@ -466,54 +473,6 @@ TEST_F(WebRPCServerHandlerTest, WsNotReady)
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
} }
TEST_F(WebRPCServerHandlerTest, HTTPInvalidAPIVersion)
{
static auto constexpr request = R"({
"method": "server_info",
"params": [{
"api_version": null
}]
})";
backend->setRange(MINSEQ, MAXSEQ);
static auto constexpr response = "invalid_API_version";
EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1);
(*handler)(request, session);
EXPECT_EQ(session->message, response);
EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request);
}
TEST_F(WebRPCServerHandlerTest, WSInvalidAPIVersion)
{
session->upgraded = true;
static auto constexpr request = R"({
"method": "server_info",
"api_version": null
})";
backend->setRange(MINSEQ, MAXSEQ);
static auto constexpr response = R"({
"error": "invalid_API_version",
"error_code": 6000,
"error_message": "API version must be an integer",
"status": "error",
"type": "response",
"request": {
"method": "server_info",
"api_version": null
}
})";
EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1);
(*handler)(request, session);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCServerHandlerTest, HTTPBadSyntaxWhenRequestSubscribe) TEST_F(WebRPCServerHandlerTest, HTTPBadSyntaxWhenRequestSubscribe)
{ {
static auto constexpr request = R"({"method": "subscribe"})"; static auto constexpr request = R"({"method": "subscribe"})";
@@ -872,3 +831,84 @@ TEST_F(WebRPCServerHandlerTest, WsRequestNotJson)
(*handler)(request, session); (*handler)(request, session);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
} }
struct InvalidAPIVersionTestBundle {
std::string testName;
std::string version;
std::string wsMessage;
};
// parameterized test cases for parameters check
struct WebRPCServerHandlerInvalidAPIVersionParamTest : public WebRPCServerHandlerTest,
public testing::WithParamInterface<InvalidAPIVersionTestBundle> {
};
auto
generateInvalidVersions()
{
return std::vector<InvalidAPIVersionTestBundle>{
{"v0", "0", fmt::format("Requested API version is lower than minimum supported ({})", rpc::API_VERSION_MIN)},
{"v4", "4", fmt::format("Requested API version is higher than maximum supported ({})", rpc::API_VERSION_MAX)},
{"null", "null", "API version must be an integer"},
{"str", "\"bogus\"", "API version must be an integer"},
{"bool", "false", "API version must be an integer"},
{"double", "12.34", "API version must be an integer"},
};
}
INSTANTIATE_TEST_CASE_P(
WebRPCServerHandlerAPIVersionGroup,
WebRPCServerHandlerInvalidAPIVersionParamTest,
testing::ValuesIn(generateInvalidVersions()),
tests::util::NameGenerator
);
TEST_P(WebRPCServerHandlerInvalidAPIVersionParamTest, HTTPInvalidAPIVersion)
{
auto request = fmt::format(
R"({{
"method": "server_info",
"params": [{{
"api_version": {}
}}]
}})",
GetParam().version
);
backend->setRange(MINSEQ, MAXSEQ);
EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1);
(*handler)(request, session);
EXPECT_EQ(session->message, "invalid_API_version");
EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request);
}
TEST_P(WebRPCServerHandlerInvalidAPIVersionParamTest, WSInvalidAPIVersion)
{
session->upgraded = true;
auto request = fmt::format(
R"({{
"method": "server_info",
"api_version": {}
}})",
GetParam().version
);
backend->setRange(MINSEQ, MAXSEQ);
EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1);
(*handler)(request, session);
auto response = boost::json::parse(session->message);
EXPECT_TRUE(response.is_object());
EXPECT_TRUE(response.as_object().contains("error"));
EXPECT_EQ(response.at("error").as_string(), "invalid_API_version");
EXPECT_TRUE(response.as_object().contains("error_message"));
EXPECT_EQ(response.at("error_message").as_string(), GetParam().wsMessage);
EXPECT_TRUE(response.as_object().contains("error_code"));
EXPECT_EQ(response.at("error_code").as_int64(), static_cast<int64_t>(rpc::ClioError::rpcINVALID_API_VERSION));
}
} // namespace

View File

@@ -56,6 +56,7 @@
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <thread> #include <thread>
#include <tuple>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -214,8 +215,9 @@ TEST_F(WebServerTest, Http)
{ {
auto e = std::make_shared<EchoExecutor>(); auto e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e); auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const res = HttpSyncClient::post("localhost", port, R"({"Hello":1})"); auto const [status, res] = HttpSyncClient::post("localhost", port, R"({"Hello":1})");
EXPECT_EQ(res, R"({"Hello":1})"); EXPECT_EQ(res, R"({"Hello":1})");
EXPECT_EQ(status, boost::beast::http::status::ok);
} }
TEST_F(WebServerTest, Ws) TEST_F(WebServerTest, Ws)
@@ -233,11 +235,12 @@ TEST_F(WebServerTest, HttpInternalError)
{ {
auto e = std::make_shared<ExceptionExecutor>(); auto e = std::make_shared<ExceptionExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e); auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const res = HttpSyncClient::post("localhost", port, R"({})"); auto const [status, res] = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ( EXPECT_EQ(
res, res,
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})" R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})"
); );
EXPECT_EQ(status, boost::beast::http::status::internal_server_error);
} }
TEST_F(WebServerTest, WsInternalError) TEST_F(WebServerTest, WsInternalError)
@@ -316,13 +319,16 @@ TEST_F(WebServerTest, HttpRequestOverload)
{ {
auto e = std::make_shared<EchoExecutor>(); auto e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuardOverload, e); auto const server = makeServerSync(cfg, ctx, dosGuardOverload, e);
auto res = HttpSyncClient::post("localhost", port, R"({})"); auto [status, res] = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ(res, "{}"); EXPECT_EQ(res, "{}");
res = HttpSyncClient::post("localhost", port, R"({})"); EXPECT_EQ(status, boost::beast::http::status::ok);
std::tie(status, res) = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ( EXPECT_EQ(
res, res,
R"({"error":"slowDown","error_code":10,"error_message":"You are placing too much load on the server.","status":"error","type":"response"})" R"({"error":"slowDown","error_code":10,"error_message":"You are placing too much load on the server.","status":"error","type":"response"})"
); );
EXPECT_EQ(status, boost::beast::http::status::service_unavailable);
} }
TEST_F(WebServerTest, WsRequestOverload) TEST_F(WebServerTest, WsRequestOverload)
@@ -349,11 +355,12 @@ TEST_F(WebServerTest, HttpPayloadOverload)
std::string const s100(100, 'a'); std::string const s100(100, 'a');
auto e = std::make_shared<EchoExecutor>(); auto e = std::make_shared<EchoExecutor>();
auto server = makeServerSync(cfg, ctx, dosGuardOverload, e); auto server = makeServerSync(cfg, ctx, dosGuardOverload, e);
auto const res = HttpSyncClient::post("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100)); auto const [status, res] = HttpSyncClient::post("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100));
EXPECT_EQ( EXPECT_EQ(
res, res,
R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})" R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})"
); );
EXPECT_EQ(status, boost::beast::http::status::ok);
} }
TEST_F(WebServerTest, WsPayloadOverload) TEST_F(WebServerTest, WsPayloadOverload)
@@ -393,6 +400,26 @@ TEST_F(WebServerTest, WsTooManyConnection)
EXPECT_TRUE(exceptionThrown); EXPECT_TRUE(exceptionThrown);
} }
TEST_F(WebServerTest, HealthCheck)
{
auto e = std::make_shared<ExceptionExecutor>(); // request handled before we get to executor
auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/health");
EXPECT_FALSE(res.empty());
EXPECT_EQ(status, boost::beast::http::status::ok);
}
TEST_F(WebServerTest, GetOtherThanHealthCheck)
{
auto e = std::make_shared<ExceptionExecutor>(); // request handled before we get to executor
auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/");
EXPECT_FALSE(res.empty());
EXPECT_EQ(status, boost::beast::http::status::bad_request);
}
std::string std::string
JSONServerConfigWithAdminPassword(uint32_t const port) JSONServerConfigWithAdminPassword(uint32_t const port)
{ {
@@ -500,8 +527,11 @@ TEST_P(WebServerAdminTest, HttpAdminCheck)
auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e); auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e);
std::string const request = "Why hello"; std::string const request = "Why hello";
uint32_t const webServerPort = serverConfig.value<uint32_t>("server.port"); uint32_t const webServerPort = serverConfig.value<uint32_t>("server.port");
auto const res = HttpSyncClient::post("localhost", std::to_string(webServerPort), request, GetParam().headers); auto const [status, res] =
HttpSyncClient::post("localhost", std::to_string(webServerPort), request, GetParam().headers);
EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse)); EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse));
EXPECT_EQ(status, boost::beast::http::status::ok);
} }
INSTANTIATE_TEST_CASE_P( INSTANTIATE_TEST_CASE_P(
@@ -618,8 +648,10 @@ TEST_F(WebServerPrometheusTest, rejectedWithoutAdminPassword)
uint32_t const webServerPort = tests::util::generateFreePort(); uint32_t const webServerPort = tests::util::generateFreePort();
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))}; Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e); auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::get("localhost", std::to_string(webServerPort), "", "/metrics"); auto const [status, res] = HttpSyncClient::get("localhost", std::to_string(webServerPort), "", "/metrics");
EXPECT_EQ(res, "Only admin is allowed to collect metrics"); EXPECT_EQ(res, "Only admin is allowed to collect metrics");
EXPECT_EQ(status, boost::beast::http::status::unauthorized);
} }
TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled) TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled)
@@ -641,7 +673,7 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled)
Config const serverConfig{boost::json::parse(JSONServerConfigWithDisabledPrometheus)}; Config const serverConfig{boost::json::parse(JSONServerConfigWithDisabledPrometheus)};
PrometheusService::init(serverConfig); PrometheusService::init(serverConfig);
auto server = makeServerSync(serverConfig, ctx, dosGuard, e); auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::get( auto const [status, res] = HttpSyncClient::get(
"localhost", "localhost",
std::to_string(webServerPort), std::to_string(webServerPort),
"", "",
@@ -652,6 +684,7 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled)
)} )}
); );
EXPECT_EQ(res, "Prometheus is disabled in clio config"); EXPECT_EQ(res, "Prometheus is disabled in clio config");
EXPECT_EQ(status, boost::beast::http::status::forbidden);
} }
TEST_F(WebServerPrometheusTest, validResponse) TEST_F(WebServerPrometheusTest, validResponse)
@@ -662,7 +695,7 @@ TEST_F(WebServerPrometheusTest, validResponse)
auto e = std::make_shared<EchoExecutor>(); auto e = std::make_shared<EchoExecutor>();
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))}; Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e); auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::get( auto const [status, res] = HttpSyncClient::get(
"localhost", "localhost",
std::to_string(webServerPort), std::to_string(webServerPort),
"", "",
@@ -673,4 +706,5 @@ TEST_F(WebServerPrometheusTest, validResponse)
)} )}
); );
EXPECT_EQ(res, "# TYPE test_counter counter\ntest_counter 1\n\n"); EXPECT_EQ(res, "# TYPE test_counter counter\ntest_counter 1\n\n");
EXPECT_EQ(status, boost::beast::http::status::ok);
} }