mirror of
https://github.com/Icinga/icinga2.git
synced 2025-07-23 21:55:03 +02:00
Add unit-tests for HttpServerConnection and HTTP message classes
This commit is contained in:
parent
87b89a7175
commit
151d8a8969
@ -87,7 +87,10 @@ set(base_test_SOURCES
|
|||||||
icinga-notification.cpp
|
icinga-notification.cpp
|
||||||
icinga-perfdata.cpp
|
icinga-perfdata.cpp
|
||||||
methods-pluginnotificationtask.cpp
|
methods-pluginnotificationtask.cpp
|
||||||
|
remote-sslcert-fixture.cpp
|
||||||
remote-configpackageutility.cpp
|
remote-configpackageutility.cpp
|
||||||
|
remote-httpserverconnection.cpp
|
||||||
|
remote-httpmessage.cpp
|
||||||
remote-url.cpp
|
remote-url.cpp
|
||||||
${base_OBJS}
|
${base_OBJS}
|
||||||
$<TARGET_OBJECTS:config>
|
$<TARGET_OBJECTS:config>
|
||||||
@ -271,6 +274,30 @@ add_boost_test(base
|
|||||||
icinga_perfdata/parse_edgecases
|
icinga_perfdata/parse_edgecases
|
||||||
icinga_perfdata/empty_warn_crit_min_max
|
icinga_perfdata/empty_warn_crit_min_max
|
||||||
methods_pluginnotificationtask/truncate_long_output
|
methods_pluginnotificationtask/truncate_long_output
|
||||||
|
remote_certs/setup_certs
|
||||||
|
remote_certs/cleanup_certs
|
||||||
|
remote_httpmessage/request_parse
|
||||||
|
remote_httpmessage/request_params
|
||||||
|
remote_httpmessage/response_flush_nothrow
|
||||||
|
remote_httpmessage/response_body_reader
|
||||||
|
remote_httpmessage/response_write_empty
|
||||||
|
remote_httpmessage/response_write_fixed
|
||||||
|
remote_httpmessage/response_write_chunked
|
||||||
|
remote_httpmessage/response_sendjsonbody
|
||||||
|
remote_httpmessage/response_sendjsonerror
|
||||||
|
remote_httpserverconnection/expect_100_continue
|
||||||
|
remote_httpserverconnection/bad_request
|
||||||
|
remote_httpserverconnection/error_access_control
|
||||||
|
remote_httpserverconnection/error_accept_header
|
||||||
|
remote_httpserverconnection/error_authenticate_cn
|
||||||
|
remote_httpserverconnection/error_authenticate_passwd
|
||||||
|
remote_httpserverconnection/error_authenticate_wronguser
|
||||||
|
remote_httpserverconnection/error_authenticate_wrongpasswd
|
||||||
|
remote_httpserverconnection/reuse_connection
|
||||||
|
remote_httpserverconnection/wg_abort
|
||||||
|
remote_httpserverconnection/client_shutdown
|
||||||
|
remote_httpserverconnection/handler_throw
|
||||||
|
remote_httpserverconnection/liveness_disconnect
|
||||||
remote_configpackageutility/ValidateName
|
remote_configpackageutility/ValidateName
|
||||||
remote_url/id_and_path
|
remote_url/id_and_path
|
||||||
remote_url/parameters
|
remote_url/parameters
|
||||||
@ -279,6 +306,43 @@ add_boost_test(base
|
|||||||
remote_url/illegal_legal_strings
|
remote_url/illegal_legal_strings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
set_tests_properties(
|
||||||
|
base-remote_httpmessage/request_parse
|
||||||
|
base-remote_httpmessage/request_params
|
||||||
|
base-remote_httpmessage/response_flush_nothrow
|
||||||
|
base-remote_httpmessage/response_body_reader
|
||||||
|
base-remote_httpmessage/response_write_empty
|
||||||
|
base-remote_httpmessage/response_write_fixed
|
||||||
|
base-remote_httpmessage/response_write_chunked
|
||||||
|
base-remote_httpmessage/response_sendjsonbody
|
||||||
|
base-remote_httpmessage/response_sendjsonerror
|
||||||
|
base-remote_httpserverconnection/expect_100_continue
|
||||||
|
base-remote_httpserverconnection/bad_request
|
||||||
|
base-remote_httpserverconnection/error_access_control
|
||||||
|
base-remote_httpserverconnection/error_accept_header
|
||||||
|
base-remote_httpserverconnection/error_authenticate_cn
|
||||||
|
base-remote_httpserverconnection/error_authenticate_passwd
|
||||||
|
base-remote_httpserverconnection/error_authenticate_wronguser
|
||||||
|
base-remote_httpserverconnection/error_authenticate_wrongpasswd
|
||||||
|
base-remote_httpserverconnection/reuse_connection
|
||||||
|
base-remote_httpserverconnection/wg_abort
|
||||||
|
base-remote_httpserverconnection/client_shutdown
|
||||||
|
base-remote_httpserverconnection/handler_throw
|
||||||
|
base-remote_httpserverconnection/liveness_disconnect
|
||||||
|
PROPERTIES FIXTURES_REQUIRED ssl_certs)
|
||||||
|
|
||||||
|
set_tests_properties(
|
||||||
|
base-remote_certs/setup_certs
|
||||||
|
PROPERTIES FIXTURES_SETUP ssl_certs
|
||||||
|
)
|
||||||
|
|
||||||
|
set_tests_properties(
|
||||||
|
base-remote_certs/cleanup_certs
|
||||||
|
PROPERTIES FIXTURES_CLEANUP ssl_certs
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(ICINGA2_WITH_LIVESTATUS)
|
if(ICINGA2_WITH_LIVESTATUS)
|
||||||
set(livestatus_test_SOURCES
|
set(livestatus_test_SOURCES
|
||||||
icingaapplication-fixture.cpp
|
icingaapplication-fixture.cpp
|
||||||
|
50
test/base-configuration-fixture.hpp
Normal file
50
test/base-configuration-fixture.hpp
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
#ifndef CONFIGURATION_FIXTURE_H
|
||||||
|
#define CONFIGURATION_FIXTURE_H
|
||||||
|
|
||||||
|
#include "base/configuration.hpp"
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
#include <BoostTestTargetConfig.h>
|
||||||
|
|
||||||
|
namespace icinga {
|
||||||
|
|
||||||
|
struct ConfigurationDataDirFixture
|
||||||
|
{
|
||||||
|
ConfigurationDataDirFixture() : m_DataDir(boost::filesystem::current_path() / "data"), m_PrevDataDir(Configuration::DataDir.GetData())
|
||||||
|
{
|
||||||
|
Configuration::DataDir = m_DataDir.string();
|
||||||
|
boost::filesystem::create_directories(m_DataDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ConfigurationDataDirFixture()
|
||||||
|
{
|
||||||
|
boost::filesystem::remove_all(m_DataDir);
|
||||||
|
Configuration::DataDir = m_PrevDataDir.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::filesystem::path m_DataDir;
|
||||||
|
boost::filesystem::path m_PrevDataDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ConfigurationCacheDirFixture
|
||||||
|
{
|
||||||
|
ConfigurationCacheDirFixture() : m_CacheDir(boost::filesystem::current_path() / "data"), m_PrevCacheDir(Configuration::CacheDir.GetData())
|
||||||
|
{
|
||||||
|
Configuration::CacheDir = m_CacheDir.string();
|
||||||
|
boost::filesystem::create_directories(m_CacheDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ConfigurationCacheDirFixture()
|
||||||
|
{
|
||||||
|
boost::filesystem::remove_all(m_CacheDir);
|
||||||
|
Configuration::CacheDir = m_PrevCacheDir.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::filesystem::path m_CacheDir;
|
||||||
|
boost::filesystem::path m_PrevCacheDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace icinga
|
||||||
|
|
||||||
|
#endif // CONFIGURATION_FIXTURE_H
|
91
test/base-testloggerfixture.hpp
Normal file
91
test/base-testloggerfixture.hpp
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
#ifndef TEST_LOGGER_FIXTURE_H
|
||||||
|
#define TEST_LOGGER_FIXTURE_H
|
||||||
|
|
||||||
|
#include "base/logger.hpp"
|
||||||
|
#include <boost/test/test_tools.hpp>
|
||||||
|
#include <boost/regex.hpp>
|
||||||
|
#include <future>
|
||||||
|
#include <BoostTestTargetConfig.h>
|
||||||
|
|
||||||
|
namespace icinga {
|
||||||
|
|
||||||
|
class TestLogger : public Logger
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DECLARE_PTR_TYPEDEFS(TestLogger);
|
||||||
|
|
||||||
|
struct Expect
|
||||||
|
{
|
||||||
|
std::string pattern;
|
||||||
|
std::promise<bool> prom;
|
||||||
|
std::shared_future<bool> crutch;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto ExpectLogPattern(const std::string& pattern, const std::chrono::milliseconds& timeout = std::chrono::seconds(0))
|
||||||
|
{
|
||||||
|
std::unique_lock lock(m_Mutex);
|
||||||
|
for (const auto & logEntry : m_LogEntries) {
|
||||||
|
if (boost::regex_match(logEntry.Message.GetData(), boost::regex(pattern))) {
|
||||||
|
return boost::test_tools::assertion_result{true};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout == std::chrono::seconds(0)) {
|
||||||
|
return boost::test_tools::assertion_result{false};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto prom = std::promise<bool>();
|
||||||
|
auto fut = prom.get_future().share();
|
||||||
|
m_Expects.emplace_back(Expect{pattern, std::move(prom), fut});
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
auto status = fut.wait_for(timeout);
|
||||||
|
boost::test_tools::assertion_result ret{status == std::future_status::ready && fut.get()};
|
||||||
|
ret.message() << "Pattern \"" << pattern << "\" in log within " << timeout.count() << "ms";
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ProcessLogEntry(const LogEntry& entry) override
|
||||||
|
{
|
||||||
|
std::unique_lock lock(m_Mutex);
|
||||||
|
m_LogEntries.push_back(entry);
|
||||||
|
|
||||||
|
m_Expects.erase(std::remove_if(m_Expects.begin(), m_Expects.end(), [&entry](Expect& expect) {
|
||||||
|
if (boost::regex_match(entry.Message.GetData(), boost::regex(expect.pattern))) {
|
||||||
|
expect.prom.set_value(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}), m_Expects.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Flush() override {}
|
||||||
|
|
||||||
|
std::mutex m_Mutex;
|
||||||
|
std::vector<Expect> m_Expects;
|
||||||
|
std::vector<LogEntry> m_LogEntries;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TestLoggerFixture
|
||||||
|
{
|
||||||
|
TestLoggerFixture()
|
||||||
|
{
|
||||||
|
testLogger->SetSeverity(testLogger->SeverityToString(LogDebug));
|
||||||
|
testLogger->Activate(true);
|
||||||
|
testLogger->SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ExpectLogPattern(const std::string& pattern, const std::chrono::milliseconds& timeout = std::chrono::seconds(0))
|
||||||
|
{
|
||||||
|
return testLogger->ExpectLogPattern(pattern, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestLogger::Ptr testLogger = new TestLogger;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace icinga
|
||||||
|
|
||||||
|
#endif // TEST_LOGGER_FIXTURE_H
|
105
test/base-tlsstream-fixture.hpp
Normal file
105
test/base-tlsstream-fixture.hpp
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
#ifndef TLSSTREAM_FIXTURE_H
|
||||||
|
#define TLSSTREAM_FIXTURE_H
|
||||||
|
|
||||||
|
#include "remote-sslcert-fixture.hpp"
|
||||||
|
#include "base/tlsstream.hpp"
|
||||||
|
#include "base/io-engine.hpp"
|
||||||
|
#include <future>
|
||||||
|
#include <BoostTestTargetConfig.h>
|
||||||
|
|
||||||
|
namespace icinga {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a pair of TLS Streams on a random unused port.
|
||||||
|
*/
|
||||||
|
struct TlsStreamFixture: CertificateFixture
|
||||||
|
{
|
||||||
|
TlsStreamFixture()
|
||||||
|
{
|
||||||
|
using namespace boost::asio::ip;
|
||||||
|
using handshake_type = boost::asio::ssl::stream_base::handshake_type;
|
||||||
|
|
||||||
|
auto serverCert = EnsureCertFor("server");
|
||||||
|
auto clientCert = EnsureCertFor("client");
|
||||||
|
|
||||||
|
auto& serverIoContext = IoEngine::Get().GetIoContext();
|
||||||
|
|
||||||
|
clientSslContext = SetupSslContext(clientCert.crtFile, clientCert.keyFile,
|
||||||
|
m_CaCrtFile.string(), "", DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
|
||||||
|
client = Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *clientSslContext);
|
||||||
|
|
||||||
|
serverSslContext = SetupSslContext(serverCert.crtFile, serverCert.keyFile,
|
||||||
|
m_CaCrtFile.string(), "", DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
|
||||||
|
server = Shared<AsioTlsStream>::Make(serverIoContext, *serverSslContext);
|
||||||
|
|
||||||
|
std::promise<void> p;
|
||||||
|
|
||||||
|
tcp::acceptor acceptor{serverIoContext, tcp::endpoint{address_v4::loopback(), 0}};
|
||||||
|
acceptor.listen();
|
||||||
|
acceptor.async_accept(server->lowest_layer(), [&, this](const boost::system::error_code& ec) {
|
||||||
|
if (ec) {
|
||||||
|
BOOST_TEST_MESSAGE("Server Accept Error: " + ec.message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
server->next_layer().async_handshake(handshake_type::server, [&, this](const boost::system::error_code& ec) {
|
||||||
|
if (ec) {
|
||||||
|
BOOST_TEST_MESSAGE("Server Handshake Error: " + ec.message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.set_value();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
boost::system::error_code ec;
|
||||||
|
if (client->lowest_layer().connect(acceptor.local_endpoint(), ec)) {
|
||||||
|
BOOST_TEST_MESSAGE("Client Connect error: " + ec.message());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client->next_layer().handshake(handshake_type::client, ec)) {
|
||||||
|
BOOST_TEST_MESSAGE("Client Handshake error: " + ec.message());
|
||||||
|
}
|
||||||
|
if (!client->next_layer().IsVerifyOK()) {
|
||||||
|
BOOST_TEST_MESSAGE("Verify failed for connection");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.get_future().wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Shutdown(const Shared<AsioTlsStream>::Ptr& stream, std::optional<boost::asio::yield_context> yc = {})
|
||||||
|
{
|
||||||
|
boost::system::error_code ec;
|
||||||
|
if (yc) {
|
||||||
|
stream->next_layer().async_shutdown((*yc)[ec]);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
stream->next_layer().shutdown(ec);
|
||||||
|
}
|
||||||
|
#if BOOST_VERSION < 107000
|
||||||
|
/* On boost versions < 1.70, the end-of-file condition was propagated as an error,
|
||||||
|
* even in case of a successful shutdown. This is information can be found in the
|
||||||
|
* changelog for the boost Asio 1.14.0 / Boost 1.70 release.
|
||||||
|
*/
|
||||||
|
if (ec == boost::asio::error::eof) {
|
||||||
|
BOOST_TEST_MESSAGE("Shutdown completed successfully with 'boost::asio::error::eof'.");
|
||||||
|
return boost::test_tools::assertion_result{true};
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
boost::test_tools::assertion_result ret{!ec};
|
||||||
|
ret.message() << "Error: " << ec.message();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
Shared<boost::asio::ssl::context>::Ptr clientSslContext;
|
||||||
|
Shared<AsioTlsStream>::Ptr client;
|
||||||
|
|
||||||
|
Shared<boost::asio::ssl::context>::Ptr serverSslContext;
|
||||||
|
Shared<AsioTlsStream>::Ptr server;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace icinga
|
||||||
|
|
||||||
|
#endif // TLSSTREAM_FIXTURE_H
|
@ -26,6 +26,8 @@ void IcingaApplicationFixture::InitIcingaApplication()
|
|||||||
|
|
||||||
IcingaApplicationFixture::~IcingaApplicationFixture()
|
IcingaApplicationFixture::~IcingaApplicationFixture()
|
||||||
{
|
{
|
||||||
|
BOOST_TEST_MESSAGE("Uninitializing Application...");
|
||||||
|
Application::UninitializeBase();
|
||||||
IcingaApplication::GetInstance().reset();
|
IcingaApplication::GetInstance().reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
258
test/remote-httpmessage.cpp
Normal file
258
test/remote-httpmessage.cpp
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
#include "test/base-tlsstream-fixture.hpp"
|
||||||
|
#include "remote/httpmessage.hpp"
|
||||||
|
#include "base/base64.hpp"
|
||||||
|
#include "base/json.hpp"
|
||||||
|
#include <BoostTestTargetConfig.h>
|
||||||
|
|
||||||
|
using namespace icinga;
|
||||||
|
using namespace boost::beast;
|
||||||
|
|
||||||
|
BOOST_FIXTURE_TEST_SUITE(remote_httpmessage, TlsStreamFixture)
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(request_parse)
|
||||||
|
{
|
||||||
|
http::request<boost::beast::http::string_body> requestOut;
|
||||||
|
requestOut.method(http::verb::get);
|
||||||
|
requestOut.target("https://localhost:5665/v1/test");
|
||||||
|
requestOut.set(http::field::authorization, "Basic " + Base64::Encode("invalid:invalid"));
|
||||||
|
requestOut.set(http::field::accept, "application/json");
|
||||||
|
requestOut.set(http::field::connection, "close");
|
||||||
|
requestOut.content_length(0);
|
||||||
|
|
||||||
|
auto & io = IoEngine::Get();
|
||||||
|
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||||
|
boost::beast::flat_buffer buf;
|
||||||
|
HttpRequest request(server);
|
||||||
|
server->async_fill(yc);
|
||||||
|
BOOST_REQUIRE_NO_THROW(request.ParseHeader(buf, yc));
|
||||||
|
|
||||||
|
for (auto & field : requestOut.base()) {
|
||||||
|
BOOST_REQUIRE(request.count(field.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
request.ParseBody(buf, yc);
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(server, yc));
|
||||||
|
});
|
||||||
|
|
||||||
|
http::write(*client, requestOut);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(request_params)
|
||||||
|
{
|
||||||
|
HttpRequest request(client);
|
||||||
|
request.body() = JsonEncode(new Dictionary{{"test1", false}, {"test2", true}});
|
||||||
|
request.target("https://localhost:1234/v1/test?test1=1&test3=0&test3=1");
|
||||||
|
request.DecodeParams();
|
||||||
|
|
||||||
|
BOOST_REQUIRE(!request.Params()->Get("test2").IsObjectType<Array>());
|
||||||
|
BOOST_REQUIRE(request.Params()->Get("test2").IsBoolean());
|
||||||
|
BOOST_REQUIRE(request.Params()->Get("test2").ToBool());
|
||||||
|
BOOST_REQUIRE(request.Params()->Get("test1").IsObjectType<Array>());
|
||||||
|
BOOST_REQUIRE(request.GetLastParameter("test1"));
|
||||||
|
BOOST_REQUIRE(request.Params()->Get("test3").IsObjectType<Array>());
|
||||||
|
BOOST_REQUIRE(request.GetLastParameter("test3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(response_body_reader)
|
||||||
|
{
|
||||||
|
auto & io = IoEngine::Get();
|
||||||
|
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||||
|
HttpResponse response(server);
|
||||||
|
response.result(http::status::ok);
|
||||||
|
response.StartStreaming();
|
||||||
|
|
||||||
|
SerializableBody<boost::beast::multi_buffer>::reader rd{response.base(), response.body()};
|
||||||
|
boost::system::error_code ec;
|
||||||
|
rd.init(3, ec);
|
||||||
|
BOOST_REQUIRE(!ec);
|
||||||
|
|
||||||
|
boost::asio::const_buffer seq1("test1", 5);
|
||||||
|
rd.put(seq1, ec);
|
||||||
|
BOOST_REQUIRE(!ec);
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
|
||||||
|
boost::asio::const_buffer seq2("test2", 5);
|
||||||
|
rd.put(seq2, ec);
|
||||||
|
BOOST_REQUIRE(!ec);
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
|
||||||
|
rd.finish(ec);
|
||||||
|
BOOST_REQUIRE(!ec);
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(server, yc));
|
||||||
|
});
|
||||||
|
|
||||||
|
http::response_parser<http::string_body> parser;
|
||||||
|
flat_buffer buf;
|
||||||
|
http::read(*client, buf, parser);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), true);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().body(), "test1test2");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(response_flush_nothrow)
|
||||||
|
{
|
||||||
|
auto& io = IoEngine::Get();
|
||||||
|
std::promise<void> done;
|
||||||
|
|
||||||
|
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||||
|
HttpResponse response(server);
|
||||||
|
response.result(http::status::ok);
|
||||||
|
|
||||||
|
server->lowest_layer().close();
|
||||||
|
|
||||||
|
boost::beast::error_code ec;
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc[ec]));
|
||||||
|
BOOST_REQUIRE_EQUAL(ec, boost::system::errc::bad_file_descriptor);
|
||||||
|
|
||||||
|
done.set_value();
|
||||||
|
});
|
||||||
|
|
||||||
|
auto status = done.get_future().wait_for(std::chrono::seconds(1));
|
||||||
|
|
||||||
|
BOOST_REQUIRE(status == std::future_status::ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(response_write_empty)
|
||||||
|
{
|
||||||
|
auto & io = IoEngine::Get();
|
||||||
|
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||||
|
HttpResponse response(server);
|
||||||
|
response.result(http::status::ok);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(server, yc));
|
||||||
|
});
|
||||||
|
|
||||||
|
http::response_parser<http::string_body> parser;
|
||||||
|
flat_buffer buf;
|
||||||
|
http::read(*client, buf, parser);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().body(), "");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(response_write_fixed)
|
||||||
|
{
|
||||||
|
auto & io = IoEngine::Get();
|
||||||
|
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||||
|
HttpResponse response(server);
|
||||||
|
response.result(http::status::ok);
|
||||||
|
response.body() << "test";
|
||||||
|
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(server, yc));
|
||||||
|
});
|
||||||
|
|
||||||
|
http::response_parser<http::string_body> parser;
|
||||||
|
flat_buffer buf;
|
||||||
|
http::read(*client, buf, parser);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().body(), "test");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(response_write_chunked)
|
||||||
|
{
|
||||||
|
auto & io = IoEngine::Get();
|
||||||
|
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||||
|
HttpResponse response(server);
|
||||||
|
response.result(http::status::ok);
|
||||||
|
|
||||||
|
response.StartStreaming();
|
||||||
|
response.body() << "test" << 1;
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
|
||||||
|
response.body() << "test" << 2;
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
|
||||||
|
response.body() << "test" << 3;
|
||||||
|
response.body().Finish();
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(server, yc));
|
||||||
|
});
|
||||||
|
|
||||||
|
http::response_parser<http::string_body> parser;
|
||||||
|
flat_buffer buf;
|
||||||
|
http::read(*client, buf, parser);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), true);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().body(), "test1test2test3");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(response_sendjsonbody)
|
||||||
|
{
|
||||||
|
auto & io = IoEngine::Get();
|
||||||
|
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||||
|
HttpResponse response(server);
|
||||||
|
response.result(http::status::ok);
|
||||||
|
|
||||||
|
response.SendJsonBody(new Dictionary{{"test", 1}});
|
||||||
|
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
BOOST_REQUIRE(Shutdown(server, yc));
|
||||||
|
});
|
||||||
|
|
||||||
|
http::response_parser<http::string_body> parser;
|
||||||
|
flat_buffer buf;
|
||||||
|
http::read(*client, buf, parser);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||||
|
Dictionary::Ptr body = JsonDecode(parser.get().body());
|
||||||
|
BOOST_REQUIRE_EQUAL(body->Get("test"), 1);
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(response_sendjsonerror)
|
||||||
|
{
|
||||||
|
auto & io = IoEngine::Get();
|
||||||
|
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||||
|
HttpResponse response(server);
|
||||||
|
|
||||||
|
// This has to be overwritten in SendJsonError.
|
||||||
|
response.result(http::status::ok);
|
||||||
|
|
||||||
|
response.SendJsonError(404, "Not found.");
|
||||||
|
|
||||||
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||||
|
BOOST_REQUIRE(Shutdown(server, yc));
|
||||||
|
});
|
||||||
|
|
||||||
|
http::response_parser<http::string_body> parser;
|
||||||
|
flat_buffer buf;
|
||||||
|
http::read(*client, buf, parser);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::not_found);
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||||
|
Dictionary::Ptr body = JsonDecode(parser.get().body());
|
||||||
|
BOOST_REQUIRE_EQUAL(body->Get("error"), 404);
|
||||||
|
BOOST_REQUIRE_EQUAL(body->Get("status"), "Not found.");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_SUITE_END()
|
482
test/remote-httpserverconnection.cpp
Normal file
482
test/remote-httpserverconnection.cpp
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
#include "base-tlsstream-fixture.hpp"
|
||||||
|
#include "icingaapplication-fixture.hpp"
|
||||||
|
#include "base-testloggerfixture.hpp"
|
||||||
|
#include "base/base64.hpp"
|
||||||
|
#include "base/json.hpp"
|
||||||
|
#include "remote/httphandler.hpp"
|
||||||
|
#include <boost/beast/http.hpp>
|
||||||
|
#include <boost/algorithm/string.hpp>
|
||||||
|
#include <boost/container/flat_set.hpp>
|
||||||
|
#include <boost/version.hpp>
|
||||||
|
#include <BoostTestTargetConfig.h>
|
||||||
|
|
||||||
|
using namespace icinga;
|
||||||
|
using namespace boost::beast;
|
||||||
|
// using namespace boost::unit_test;
|
||||||
|
using namespace boost::unit_test_framework;
|
||||||
|
|
||||||
|
struct HttpServerConnectionFixture : TlsStreamFixture,
|
||||||
|
IcingaApplicationFixture,
|
||||||
|
ConfigurationCacheDirFixture,
|
||||||
|
TestLoggerFixture
|
||||||
|
{
|
||||||
|
HttpServerConnection::Ptr conn;
|
||||||
|
StoppableWaitGroup::Ptr wg;
|
||||||
|
|
||||||
|
HttpServerConnectionFixture()
|
||||||
|
: wg(new StoppableWaitGroup)
|
||||||
|
{
|
||||||
|
Logger::SetConsoleLogSeverity(icinga::LogDebug);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CreateApiListener()
|
||||||
|
{
|
||||||
|
ScriptGlobal::Set("NodeName", "server");
|
||||||
|
ApiListener::Ptr listener = new ApiListener;
|
||||||
|
listener->OnConfigLoaded();
|
||||||
|
listener->SetAccessControlAllowOrigin(new Array{"127.0.0.1"});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CreateTestUsers()
|
||||||
|
{
|
||||||
|
ApiUser::Ptr user = new ApiUser;
|
||||||
|
user->SetName("client");
|
||||||
|
user->SetClientCN("client");
|
||||||
|
user->SetPermissions(new Array{"*"});
|
||||||
|
user->Register();
|
||||||
|
|
||||||
|
user = new ApiUser;
|
||||||
|
user->SetName("test");
|
||||||
|
user->SetPassword("test");
|
||||||
|
user->SetPermissions(new Array{"*"});
|
||||||
|
user->Register();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetupHttpServerConnection(bool authenticated)
|
||||||
|
{
|
||||||
|
String identity = authenticated ? "client" : "invalid";
|
||||||
|
conn = new HttpServerConnection(wg, identity, authenticated, server);
|
||||||
|
conn->Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Rep, class Period>
|
||||||
|
bool AssertServerDisconnected(const std::chrono::duration<Rep, Period>& timeout)
|
||||||
|
{
|
||||||
|
auto iterations = timeout / std::chrono::milliseconds(50);
|
||||||
|
for(std::size_t i=0; i<iterations && !conn->Disconnected(); i++){
|
||||||
|
Utility::Sleep(std::chrono::duration<double>(timeout).count()/iterations);
|
||||||
|
}
|
||||||
|
return conn->Disconnected();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class UnitTestHandler final: public HttpHandler
|
||||||
|
{
|
||||||
|
|
||||||
|
bool HandleRequest(const WaitGroup::Ptr& waitGroup, const HttpRequest& request, HttpResponse& response,
|
||||||
|
boost::asio::yield_context& yc) override
|
||||||
|
{
|
||||||
|
response.result(boost::beast::http::status::ok);
|
||||||
|
|
||||||
|
if (request.GetLastParameter("stream")) {
|
||||||
|
response.StartStreaming();
|
||||||
|
response.Flush(yc);
|
||||||
|
for (;;) {
|
||||||
|
response.body() << "test";
|
||||||
|
response.Flush(yc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.GetLastParameter("throw")) {
|
||||||
|
response.StartStreaming();
|
||||||
|
response.body() << "test";
|
||||||
|
response.Flush(yc);
|
||||||
|
throw std::runtime_error{"The front fell off"};
|
||||||
|
}
|
||||||
|
|
||||||
|
response.body() << "test";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
REGISTER_URLHANDLER("/v1/test", UnitTestHandler);
|
||||||
|
|
||||||
|
BOOST_FIXTURE_TEST_SUITE(remote_httpserverconnection, HttpServerConnectionFixture)
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(expect_100_continue)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(true);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.version(11);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::expect, "100-continue");
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.set(http::field::connection, "close");
|
||||||
|
request.content_length(0);
|
||||||
|
http::request_serializer<http::string_body> sr(request);
|
||||||
|
http::write_header(*client, sr);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::continue_);
|
||||||
|
|
||||||
|
http::write(*client, sr);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.body(), "test");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(bad_request)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(true);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.version(12);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.set(http::field::connection, "close");
|
||||||
|
request.content_length(0);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::bad_request);
|
||||||
|
BOOST_REQUIRE_NE(response.body().find("<h1>Bad Request</h1>"), std::string::npos);
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(error_access_control)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
CreateApiListener();
|
||||||
|
SetupHttpServerConnection(true);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::options);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::origin, "127.0.0.1");
|
||||||
|
request.set(http::field::access_control_request_method, "GET");
|
||||||
|
request.set(http::field::connection, "close");
|
||||||
|
request.content_length(0);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.body(), "Preflight OK");
|
||||||
|
|
||||||
|
boost::container::flat_set<std::string> sv;
|
||||||
|
boost::container::flat_set options{"GET", "POST", "PUT", "DELETE", "PUSH"};
|
||||||
|
|
||||||
|
BOOST_REQUIRE_NE(response[http::field::access_control_allow_methods], "");
|
||||||
|
BOOST_REQUIRE_NE(response[http::field::access_control_allow_headers], "");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(error_accept_header)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(true);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::post);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::accept, "text/html");
|
||||||
|
request.set(http::field::connection, "close");
|
||||||
|
request.content_length(0);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::bad_request);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.body(), "<h1>Accept header is missing or not set to 'application/json'.</h1>\r\n");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(error_authenticate_cn)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(true);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.set(http::field::connection, "close");
|
||||||
|
request.content_length(0);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(error_authenticate_passwd)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(false);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::authorization, "Basic " + Base64::Encode("test:test"));
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.set(http::field::connection, "close");
|
||||||
|
request.content_length(0);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(error_authenticate_wronguser)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(false);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::authorization, "Basic " + Base64::Encode("invalid:invalid"));
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.set(http::field::connection, "close");
|
||||||
|
request.content_length(0);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::unauthorized);
|
||||||
|
Dictionary::Ptr body = JsonDecode(response.body());
|
||||||
|
BOOST_REQUIRE(body);
|
||||||
|
BOOST_REQUIRE_EQUAL(body->Get("error"), 401);
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(error_authenticate_wrongpasswd)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(false);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::authorization, "Basic " + Base64::Encode("test:invalid"));
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.set(http::field::connection, "close");
|
||||||
|
request.content_length(0);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::unauthorized);
|
||||||
|
Dictionary::Ptr body = JsonDecode(response.body());
|
||||||
|
BOOST_REQUIRE(body);
|
||||||
|
BOOST_REQUIRE_EQUAL(body->Get("error"), 401);
|
||||||
|
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(reuse_connection)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(true);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.keep_alive(true);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.body(), "test");
|
||||||
|
|
||||||
|
request.keep_alive(false);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
response.body() = "";
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.body(), "test");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(5)));
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected .*", std::chrono::seconds(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(wg_abort)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(true);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.keep_alive(true);
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
wg->Join();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response<http::string_body> response;
|
||||||
|
http::read(*client, buf, response);
|
||||||
|
|
||||||
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||||
|
BOOST_REQUIRE_EQUAL(response.body(), "test");
|
||||||
|
|
||||||
|
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(5)));
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected .*", std::chrono::seconds(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(client_shutdown)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(true);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.keep_alive(true);
|
||||||
|
|
||||||
|
/* Instruct the test HttpHandler defined above to stream an endless response.
|
||||||
|
*/
|
||||||
|
request.body() = JsonEncode(new Dictionary{{"stream", true}});
|
||||||
|
request.prepare_payload();
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
/* Unlike the other test cases we don't require success here, because with the request
|
||||||
|
* above, UnitTestHandler simulates a HttpHandler that is constantly writing.
|
||||||
|
* That will cause the shutdown to fail on the client-side with "application data after
|
||||||
|
* close notify", but the important part is that HttpServerConnection actually closes
|
||||||
|
* the connection on its own side, which we check with the BOOST_REQUIRE() below.
|
||||||
|
*/
|
||||||
|
BOOST_WARN(Shutdown(client));
|
||||||
|
|
||||||
|
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(5)));
|
||||||
|
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected.*", std::chrono::seconds(5)));
|
||||||
|
BOOST_REQUIRE(ExpectLogPattern("Detected shutdown from client: .*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(handler_throw)
|
||||||
|
{
|
||||||
|
CreateTestUsers();
|
||||||
|
SetupHttpServerConnection(true);
|
||||||
|
|
||||||
|
http::request<boost::beast::http::string_body> request;
|
||||||
|
request.method(http::verb::get);
|
||||||
|
request.target("https://localhost:5665/v1/test");
|
||||||
|
request.set(http::field::accept, "application/json");
|
||||||
|
request.keep_alive(true);
|
||||||
|
|
||||||
|
/* Instruct the test TestHandler to throw an exception while streaming, which the
|
||||||
|
* HttpHandler base should propagate up to HttpServerConnection.
|
||||||
|
* The correct response is to shutdown the connection instead of trying to send a JSON
|
||||||
|
* error message.
|
||||||
|
*/
|
||||||
|
request.body() = JsonEncode(new Dictionary{{"throw", true}});
|
||||||
|
request.prepare_payload();
|
||||||
|
http::write(*client, request);
|
||||||
|
client->flush();
|
||||||
|
|
||||||
|
flat_buffer buf;
|
||||||
|
http::response_parser<http::string_body> parser;
|
||||||
|
boost::system::error_code ec;
|
||||||
|
http::read(*client, buf, parser, ec);
|
||||||
|
|
||||||
|
/* Since the handler threw in the middle of sending the message we shouldn't be able
|
||||||
|
* to read a complete message here.
|
||||||
|
* Somehow boost::beast error codes can not be tested with BOOST_REQUIRE_EQUAL so we
|
||||||
|
* compare the string instead.
|
||||||
|
*/
|
||||||
|
BOOST_REQUIRE_EQUAL(ec.message(), "partial message");
|
||||||
|
|
||||||
|
/* The body should only contain the single "test" the handler has written, without any
|
||||||
|
* attempts made to additionally write some json error message.
|
||||||
|
*/
|
||||||
|
BOOST_REQUIRE_EQUAL(parser.get().body(), "test");
|
||||||
|
|
||||||
|
/* We then expect the server to initiate a shutdown, which we then complete below.
|
||||||
|
*/
|
||||||
|
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(5)));
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected.*", std::chrono::seconds(5)));
|
||||||
|
BOOST_REQUIRE(ExpectLogPattern("Exception while processing HTTP request from .+?: The front fell off"));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(liveness_disconnect)
|
||||||
|
{
|
||||||
|
SetupHttpServerConnection(false);
|
||||||
|
|
||||||
|
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(11)));
|
||||||
|
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected .*"));
|
||||||
|
BOOST_REQUIRE(Shutdown(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_SUITE_END()
|
26
test/remote-sslcert-fixture.cpp
Normal file
26
test/remote-sslcert-fixture.cpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
#include "remote-sslcert-fixture.hpp"
|
||||||
|
#include <BoostTestTargetConfig.h>
|
||||||
|
|
||||||
|
using namespace icinga;
|
||||||
|
|
||||||
|
const boost::filesystem::path CertificateFixture::m_PersistentCertsDir = boost::filesystem::current_path() / "persistent" / "certs";
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_SUITE(remote_certs)
|
||||||
|
|
||||||
|
BOOST_FIXTURE_TEST_CASE(setup_certs, ConfigurationDataDirFixture)
|
||||||
|
{
|
||||||
|
if (boost::filesystem::exists(CertificateFixture::m_PersistentCertsDir)) {
|
||||||
|
boost::filesystem::remove_all(CertificateFixture::m_PersistentCertsDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_FIXTURE_TEST_CASE(cleanup_certs, ConfigurationDataDirFixture)
|
||||||
|
{
|
||||||
|
if (boost::filesystem::exists(CertificateFixture::m_PersistentCertsDir)) {
|
||||||
|
boost::filesystem::remove_all(CertificateFixture::m_PersistentCertsDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_SUITE_END()
|
71
test/remote-sslcert-fixture.hpp
Normal file
71
test/remote-sslcert-fixture.hpp
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
#ifndef SSLCERT_FIXTURE_H
|
||||||
|
#define SSLCERT_FIXTURE_H
|
||||||
|
|
||||||
|
#include "remote/apilistener.hpp"
|
||||||
|
#include "remote/pkiutility.hpp"
|
||||||
|
#include "test/base-configuration-fixture.hpp"
|
||||||
|
#include <BoostTestTargetConfig.h>
|
||||||
|
|
||||||
|
namespace icinga{
|
||||||
|
|
||||||
|
struct CertificateFixture: ConfigurationDataDirFixture
|
||||||
|
{
|
||||||
|
CertificateFixture()
|
||||||
|
{
|
||||||
|
namespace fs = boost::filesystem;
|
||||||
|
|
||||||
|
m_CaDir = ApiListener::GetCaDir();
|
||||||
|
m_CertsDir = ApiListener::GetCertsDir();
|
||||||
|
m_CaCrtFile = m_CertsDir / "ca.crt";
|
||||||
|
|
||||||
|
fs::create_directories(m_PersistentCertsDir / "ca");
|
||||||
|
fs::create_directories(m_PersistentCertsDir / "certs");
|
||||||
|
|
||||||
|
if (fs::exists(m_DataDir / "ca")) {
|
||||||
|
fs::remove(m_DataDir / "ca");
|
||||||
|
}
|
||||||
|
if (fs::exists(m_DataDir / "certs")) {
|
||||||
|
fs::remove(m_DataDir / "certs");
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_directory_symlink(m_PersistentCertsDir / "certs", m_DataDir / "certs");
|
||||||
|
fs::create_directory_symlink(m_PersistentCertsDir / "ca", m_DataDir / "ca");
|
||||||
|
|
||||||
|
if (!fs::exists(m_CaCrtFile)) {
|
||||||
|
PkiUtility::NewCa();
|
||||||
|
fs::copy_file(m_CaDir / "ca.crt", m_CaCrtFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto EnsureCertFor(const std::string& name)
|
||||||
|
{
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
String crtFile;
|
||||||
|
String keyFile;
|
||||||
|
String csrFile;
|
||||||
|
} cert;
|
||||||
|
|
||||||
|
cert.crtFile = (m_CertsDir/(name + ".crt")).string();
|
||||||
|
cert.keyFile = (m_CertsDir/(name + ".key")).string();
|
||||||
|
cert.csrFile = (m_CertsDir/(name + ".csr")).string();
|
||||||
|
|
||||||
|
if (!Utility::PathExists(cert.crtFile)) {
|
||||||
|
PkiUtility::NewCert(name, cert.keyFile, cert.csrFile, cert.crtFile);
|
||||||
|
PkiUtility::SignCsr(cert.csrFile, cert.crtFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::filesystem::path m_CaDir;
|
||||||
|
boost::filesystem::path m_CertsDir;
|
||||||
|
boost::filesystem::path m_CaCrtFile;
|
||||||
|
static const boost::filesystem::path m_PersistentCertsDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace icinga
|
||||||
|
|
||||||
|
#endif // SSLCERT_FIXTURE_H
|
Loading…
x
Reference in New Issue
Block a user