mirror of
https://github.com/Icinga/icinga2.git
synced 2026-02-03 20:40:17 -05:00
so that /v1/events doesn't have to use IoBoundWorkSlot. IoBoundWorkSlot#~IoBoundWorkSlot() will wait for a free semaphore slot which will be almost immediately released by CpuBoundWork#~CpuBoundWork(). Just releasing the already aquired slot in HttpApiResponse#StartStreaming() is more efficient.
269 lines
7.8 KiB
C++
269 lines
7.8 KiB
C++
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
|
|
|
#include "remote/httpmessage.hpp"
|
|
#include "base/json.hpp"
|
|
#include "remote/httputility.hpp"
|
|
#include "remote/url.hpp"
|
|
#include <boost/beast/http.hpp>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
using namespace icinga;
|
|
|
|
/**
|
|
* This is the buffer size threshold above which to flush to the connection.
|
|
*
|
|
* This value was determined with a series of measurements in
|
|
* [PR #10516](https://github.com/Icinga/icinga2/pull/10516#issuecomment-3232642284).
|
|
*/
|
|
constexpr std::size_t l_FlushThreshold = 128UL * 1024UL;
|
|
|
|
/**
|
|
* Adapter class for Boost Beast HTTP messages body to be used with the @c JsonEncoder.
|
|
*
|
|
* This class implements the @c nlohmann::detail::output_adapter_protocol<> interface and provides
|
|
* a way to write JSON data directly into the body of a @c HttpResponse.
|
|
*
|
|
* @ingroup base
|
|
*/
|
|
template<typename Message>
|
|
class HttpResponseJsonWriter : public AsyncJsonWriter
|
|
{
|
|
public:
|
|
HttpResponseJsonWriter(const HttpResponseJsonWriter&) = delete;
|
|
HttpResponseJsonWriter(HttpResponseJsonWriter&&) = delete;
|
|
HttpResponseJsonWriter& operator=(const HttpResponseJsonWriter&) = delete;
|
|
HttpResponseJsonWriter& operator=(HttpResponseJsonWriter&&) = delete;
|
|
explicit HttpResponseJsonWriter(Message& msg) : m_Message{msg}
|
|
{
|
|
m_Message.body().Start();
|
|
#if BOOST_VERSION >= 107000
|
|
// We pre-allocate more than the threshold because we always go above the threshold
|
|
// at least once.
|
|
m_Message.body().Buffer().reserve(l_FlushThreshold + (l_FlushThreshold / 4));
|
|
#endif /* BOOST_VERSION */
|
|
}
|
|
|
|
~HttpResponseJsonWriter() override { m_Message.body().Finish(); }
|
|
|
|
void write_character(char c) override { write_characters(&c, 1); }
|
|
|
|
void write_characters(const char* s, std::size_t length) override
|
|
{
|
|
auto buf = m_Message.body().Buffer().prepare(length);
|
|
boost::asio::buffer_copy(buf, boost::asio::const_buffer{s, length});
|
|
m_Message.body().Buffer().commit(length);
|
|
}
|
|
|
|
void MayFlush(boost::asio::yield_context& yield) override
|
|
{
|
|
if (m_Message.body().Size() >= l_FlushThreshold) {
|
|
m_Message.Flush(yield);
|
|
}
|
|
}
|
|
|
|
private:
|
|
Message& m_Message;
|
|
};
|
|
|
|
template<bool isRequest, typename Body, typename StreamVariant>
|
|
IncomingHttpMessage<isRequest, Body, StreamVariant>::IncomingHttpMessage(StreamVariant stream)
|
|
: m_Stream(std::move(stream))
|
|
{
|
|
}
|
|
|
|
template<bool isRequest, typename Body, typename StreamVariant>
|
|
void IncomingHttpMessage<isRequest, Body, StreamVariant>::ParseHeader(
|
|
boost::beast::flat_buffer& buf,
|
|
boost::asio::yield_context yc
|
|
)
|
|
{
|
|
std::visit([&](auto& stream) { boost::beast::http::async_read_header(*stream, buf, m_Parser, yc); }, m_Stream);
|
|
Base::base() = m_Parser.get().base();
|
|
}
|
|
|
|
template<bool isRequest, typename Body, typename StreamVariant>
|
|
void IncomingHttpMessage<isRequest, Body, StreamVariant>::ParseBody(
|
|
boost::beast::flat_buffer& buf,
|
|
boost::asio::yield_context yc
|
|
)
|
|
{
|
|
std::visit([&](auto& stream) { boost::beast::http::async_read(*stream, buf, m_Parser, yc); }, m_Stream);
|
|
Base::body() = std::move(m_Parser.release().body());
|
|
}
|
|
|
|
HttpApiRequest::HttpApiRequest(Shared<AsioTlsStream>::Ptr stream) : IncomingHttpMessage(std::move(stream))
|
|
{
|
|
}
|
|
|
|
ApiUser::Ptr HttpApiRequest::User() const
|
|
{
|
|
return m_User;
|
|
}
|
|
|
|
void HttpApiRequest::User(const ApiUser::Ptr& user)
|
|
{
|
|
m_User = user;
|
|
}
|
|
|
|
Url::Ptr HttpApiRequest::Url() const
|
|
{
|
|
return m_Url;
|
|
}
|
|
|
|
void HttpApiRequest::DecodeUrl()
|
|
{
|
|
m_Url = new icinga::Url(std::string(target()));
|
|
}
|
|
|
|
Dictionary::Ptr HttpApiRequest::Params() const
|
|
{
|
|
return m_Params;
|
|
}
|
|
|
|
void HttpApiRequest::DecodeParams()
|
|
{
|
|
if (!m_Url) {
|
|
DecodeUrl();
|
|
}
|
|
m_Params = HttpUtility::FetchRequestParameters(m_Url, body());
|
|
}
|
|
|
|
template<bool isRequest, typename Body, typename StreamVariant>
|
|
OutgoingHttpMessage<isRequest, Body, StreamVariant>::OutgoingHttpMessage(StreamVariant stream)
|
|
: m_Stream(std::move(stream))
|
|
{
|
|
}
|
|
|
|
template<bool isRequest, typename Body, typename StreamVariant>
|
|
void OutgoingHttpMessage<isRequest, Body, StreamVariant>::Clear()
|
|
{
|
|
ASSERT(!m_SerializationStarted);
|
|
Base::operator=({});
|
|
}
|
|
|
|
template<bool isRequest, typename Body, typename StreamVariant>
|
|
void OutgoingHttpMessage<isRequest, Body, StreamVariant>::Flush(boost::asio::yield_context yc, bool finish)
|
|
{
|
|
if (!Base::chunked() && !Base::has_content_length()) {
|
|
ASSERT(!m_SerializationStarted);
|
|
Base::prepare_payload();
|
|
}
|
|
|
|
std::visit(
|
|
[&](auto& stream) {
|
|
m_SerializationStarted = true;
|
|
|
|
if (!m_Serializer.is_header_done()) {
|
|
boost::beast::http::write_header(*stream, m_Serializer);
|
|
}
|
|
|
|
if (finish) {
|
|
Base::body().Finish();
|
|
}
|
|
|
|
boost::system::error_code ec;
|
|
boost::beast::http::async_write(*stream, m_Serializer, yc[ec]);
|
|
if (ec && ec != boost::beast::http::error::need_buffer) {
|
|
if (yc.ec_) {
|
|
*yc.ec_ = ec;
|
|
return;
|
|
}
|
|
BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
|
|
}
|
|
stream->async_flush(yc);
|
|
|
|
ASSERT(m_Serializer.is_done() || !Base::body().Finished());
|
|
},
|
|
m_Stream
|
|
);
|
|
}
|
|
|
|
template<bool isRequest, typename Body, typename StreamVariant>
|
|
void OutgoingHttpMessage<isRequest, Body, StreamVariant>::StartStreaming()
|
|
{
|
|
ASSERT(Base::body().Size() == 0 && !m_SerializationStarted);
|
|
Base::body().Start();
|
|
Base::chunked(true);
|
|
}
|
|
|
|
HttpApiResponse::HttpApiResponse(Shared<AsioTlsStream>::Ptr stream, HttpServerConnection::Ptr server)
|
|
: OutgoingHttpMessage(std::move(stream)), m_Server(std::move(server))
|
|
{
|
|
}
|
|
|
|
void HttpApiResponse::StartStreaming(bool checkForDisconnect)
|
|
{
|
|
OutgoingHttpMessage::StartStreaming();
|
|
|
|
if (checkForDisconnect) {
|
|
auto work (m_CpuBoundWork.lock());
|
|
|
|
if (work) {
|
|
work->Done();
|
|
}
|
|
|
|
ASSERT(m_Server);
|
|
m_Server->StartDetectClientSideShutdown();
|
|
}
|
|
}
|
|
|
|
bool HttpApiResponse::IsClientDisconnected() const
|
|
{
|
|
ASSERT(m_Server);
|
|
return m_Server->Disconnected();
|
|
}
|
|
|
|
template<bool isRequest, typename Body, typename StreamVariant>
|
|
void OutgoingHttpMessage<isRequest, Body, StreamVariant>::SendFile(
|
|
const String& path,
|
|
const boost::asio::yield_context& yc
|
|
)
|
|
{
|
|
std::ifstream fp(path.CStr(), std::ifstream::in | std::ifstream::binary | std::ifstream::ate);
|
|
fp.exceptions(std::ifstream::badbit | std::ifstream::eofbit);
|
|
|
|
std::uint64_t remaining = fp.tellg();
|
|
fp.seekg(0);
|
|
|
|
Base::content_length(remaining);
|
|
Base::body().Start();
|
|
|
|
while (remaining) {
|
|
auto maxTransfer = std::min(remaining, static_cast<std::uint64_t>(l_FlushThreshold));
|
|
|
|
using BodyBuffer = std::decay_t<decltype(std::declval<typename Body::value_type>().Buffer())>;
|
|
using BufferOrSequence = typename BodyBuffer::mutable_buffers_type;
|
|
|
|
boost::asio::mutable_buffer buf;
|
|
|
|
if constexpr (!std::is_same_v<BufferOrSequence, boost::asio::mutable_buffer>) {
|
|
buf = *Base::body().Buffer().prepare(maxTransfer).begin();
|
|
} else {
|
|
buf = Base::body().Buffer().prepare(maxTransfer);
|
|
}
|
|
fp.read(static_cast<char*>(buf.data()), buf.size());
|
|
Base::body().Buffer().commit(buf.size());
|
|
|
|
remaining -= buf.size();
|
|
Flush(yc);
|
|
}
|
|
}
|
|
|
|
template<bool isRequest, typename Body, typename StreamVariant>
|
|
JsonEncoder OutgoingHttpMessage<isRequest, Body, StreamVariant>::GetJsonEncoder(bool pretty)
|
|
{
|
|
return JsonEncoder{
|
|
std::make_shared<HttpResponseJsonWriter<OutgoingHttpMessage<isRequest, Body, StreamVariant>>>(*this), pretty
|
|
};
|
|
}
|
|
|
|
// More general instantiations
|
|
template class icinga::OutgoingHttpMessage<true, SerializableFlatBufferBody, AsioTlsOrTcpStream>;
|
|
template class icinga::OutgoingHttpMessage<false, SerializableFlatBufferBody, AsioTlsOrTcpStream>;
|
|
template class icinga::IncomingHttpMessage<true, boost::beast::http::string_body, AsioTlsOrTcpStream>;
|
|
template class icinga::IncomingHttpMessage<false, boost::beast::http::string_body, AsioTlsOrTcpStream>;
|
|
|
|
// Instantiations specifically for HttpApi(Request|Response)
|
|
template class icinga::IncomingHttpMessage<true, boost::beast::http::string_body, std::variant<Shared<AsioTlsStream>::Ptr>>;
|
|
template class icinga::OutgoingHttpMessage<false, SerializableMultiBufferBody, std::variant<Shared<AsioTlsStream>::Ptr>>;
|