// SPDX-FileCopyrightText: 2026 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include #include "base/io-engine.hpp" #include "base/json.hpp" #include "base/tlsstream.hpp" #include #include #include #include #include #include #include #include #include namespace icinga { /** * A fixture that provides methods to simulate a perfdata target */ class PerfdataWriterTargetFixture { public: PerfdataWriterTargetFixture() : icinga::PerfdataWriterTargetFixture(Shared::Make(IoEngine::Get().GetIoContext())) { } explicit PerfdataWriterTargetFixture(const Shared::Ptr& sslCtx) : icinga::PerfdataWriterTargetFixture(Shared::Make(IoEngine::Get().GetIoContext(), *sslCtx)) { m_SslContext = sslCtx; } explicit PerfdataWriterTargetFixture(AsioTlsOrTcpStream stream) : m_Stream(std::move(stream)), m_Acceptor( IoEngine::Get().GetIoContext() ) { boost::asio::ip::tcp::endpoint ep{boost::asio::ip::address_v4::loopback(), 0}; m_Acceptor.open(ep.protocol()); m_Acceptor.bind(ep); } unsigned short GetPort() { return m_Acceptor.local_endpoint().port(); } void Listen() { m_Acceptor.listen(); } void Accept() { Listen(); BOOST_REQUIRE_NO_THROW( std::visit([&](auto& stream) { return m_Acceptor.accept(stream->lowest_layer()); }, m_Stream) ); } void Handshake() { BOOST_REQUIRE(std::holds_alternative::Ptr>(m_Stream)); using handshake_type = UnbufferedAsioTlsStream::handshake_type; auto& stream = std::get::Ptr>(m_Stream); BOOST_REQUIRE_NO_THROW(stream->next_layer().handshake(handshake_type::server)); BOOST_REQUIRE(stream->next_layer().IsVerifyOK()); } void Shutdown() { BOOST_REQUIRE(std::holds_alternative::Ptr>(m_Stream)); auto& stream = std::get::Ptr>(m_Stream); try { stream->next_layer().shutdown(); } catch (const std::exception& ex) { if (const auto* se = dynamic_cast(&ex); !se || se->code() != boost::asio::error::eof) { BOOST_FAIL("Exception in shutdown(): " << ex.what()); } } ResetStream(); } void ResetStream() { if (std::holds_alternative::Ptr>(m_Stream)) { m_Stream = Shared::Make(IoEngine::Get().GetIoContext(), *m_SslContext, "localhost"); } else { m_Stream = Shared::Make(IoEngine::Get().GetIoContext()); } } /** * Reads the HTTP request body from the stream, with an optional limit on the number of bytes to read. * * @param bytes The maximum number of bytes to read from the request body. If 0, there is no limit and the entire body will be read. * * @return The HTTP request read from the stream. */ boost::beast::http::request GetRequest(std::size_t bytes = 0) { using namespace boost::beast; boost::beast::flat_buffer buf; if (bytes > 0) { buf = boost::beast::flat_buffer{bytes}; } boost::system::error_code ec; http::request_parser parser; parser.body_limit(-1); std::visit( [&](auto& stream) { http::read(*stream, buf, parser, ec); }, m_Stream ); if (bytes > 0) { BOOST_REQUIRE_MESSAGE( !ec || ec == http::error::buffer_overflow, "Reading request body with a buffer limit of '" << bytes << "' should either succeed or fail with a buffer_overflow error, but got: " << ec.message() ); } else { BOOST_REQUIRE_MESSAGE(!ec, "Error while reading request body: " << ec.message()); BOOST_REQUIRE_MESSAGE(parser.is_done(), "Parser did not finish reading the request, but no error was set."); } return parser.release(); } auto GetSplitRequestBody(char delim) { auto request = GetRequest(); std::vector result{}; boost::split(result, request.body(), boost::is_any_of(std::string{delim})); return result; } auto GetSplitDecodedRequestBody() { Array::Ptr result = new Array; for (const auto& line : GetSplitRequestBody('\n')) { if (!line.empty()) { result->Add(JsonDecode(line)); } } return result; } template std::string GetDataUntil(T&& delim) { using namespace boost::asio::ip; std::size_t delimLength{1}; if constexpr (!std::is_same_v, char>) { delimLength = std::string_view{delim}.size(); } boost::asio::streambuf buf; boost::system::error_code ec; auto bytesRead = std::visit( [&](auto& stream) { return boost::asio::read_until(*stream, buf, std::forward(delim), ec); }, m_Stream ); BOOST_REQUIRE_MESSAGE(!ec, ec.message()); std::string ret{ boost::asio::buffers_begin(buf.data()), boost::asio::buffers_begin(buf.data()) + bytesRead - delimLength }; buf.consume(bytesRead); return ret; } void SendResponse(boost::beast::http::status status = boost::beast::http::status::ok) { using namespace boost::asio::ip; using namespace boost::beast; boost::system::error_code ec; http::response response; response.result(status); response.prepare_payload(); std::visit( [&](auto& stream) { http::write(*stream, response, ec); BOOST_REQUIRE_MESSAGE(!ec, ec.message()); stream->flush(ec); BOOST_REQUIRE_MESSAGE(!ec, ec.message()); }, m_Stream ); } private: AsioTlsOrTcpStream m_Stream; boost::asio::ip::tcp::acceptor m_Acceptor; Shared::Ptr m_SslContext; }; } // namespace icinga