/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */ #ifndef HTTPMESSAGE_H #define HTTPMESSAGE_H #include "base/dictionary.hpp" #include "base/tlsstream.hpp" #include "remote/url.hpp" #include "remote/apiuser.hpp" #include #include namespace icinga { /** * A custom body_type for a @c boost::beast::http::message * * It combines the memory management of @c boost::beast::http::dynamic_body, * which uses a multi_buffer, with the ability to continue serialization when * new data arrives of the @c boost::beast::http::buffer_body. * * @tparam DynamicBuffer A buffer conforming to the boost::beast interface of the same name * * @ingroup remote */ template struct SerializableBody { class reader; class writer; class value_type { public: template value_type& operator<<(T && right) { /* Preferably, we would return an ostream object here instead. However * there seems to be a bug in boost::beast where if the ostream, or rather its * streambuf object is moved into the return value, the chunked encoding gets * mangled, leading to the client disconnecting. * * A workaround would have been to construct the boost::beast::detail::ostream_helper * with the last parameter set to false, indicating that the streambuf object is not * movable, but that is an implementation detail we'd rather not use directly in our * code. * * This version has a certain overhead of the ostream being constructed on every call * to the operator, which leads to an individual append for each time, whereas if the * object could be kept until the entire chain of output operators is finished, only * a single call to prepare()/commit() would have been needed. * * However, since this operator is mostly used for small error messages and the big * responses are handled via a reader instance, this shouldn't be too much of a * problem. */ boost::beast::ostream(buffer) << std::forward(right); return *this; } std::size_t Size() const { return buffer.size(); } void Finish() { more = false; } void Begin() { more = true; } friend class reader; friend class writer; private: /* This defaults to false so the body does not require any special handling * for simple messages and can still be written with http::async_write(). */ bool more = false; DynamicBuffer buffer; }; static std::uint64_t size(const value_type& body) { return body.Size(); } /** * Implement the boost::beast BodyReader interface for this body type * * This is used by the @c boost::beast::http::parser (which we don't use for responses) * or in the @c BeastHttpMessageAdapter for our own @c JsonEncoder to write JSON directly * into the body of a HTTP response message. * * The reader automatically sets the body's `more` flag on the call to its init() function * and resets it when its finish() function is called. Regarding the usage in the * @c JsonEncoder above, this means that the message is automatically marked as complete * when the encoder object is destroyed. */ class reader { public: template explicit reader(boost::beast::http::header& h, value_type& b) : body(b) { } void init(const boost::optional& n, boost::beast::error_code& ec) { ec = {}; body.Begin(); #if BOOST_VERSION >= 107000 if (n) { body.buffer.reserve(*n); } #endif /* BOOST_VERSION */ } template std::size_t put(const ConstBufferSequence& buffers, boost::beast::error_code& ec) { auto size = boost::asio::buffer_size(buffers); if (size > body.buffer.max_size() - body.Size()) { ec = boost::beast::http::error::buffer_overflow; return 0; } auto const wBuf = body.buffer.prepare(size); boost::asio::buffer_copy(wBuf, buffers); body.buffer.commit(size); return size; } void finish(boost::beast::error_code& ec) { ec = {}; body.Finish(); } private: value_type& body; }; /** * Implement the boost::beast BodyWriter interface for this body type * * This is used (for example) by the @c boost::beast::http::serializer to write out the * message over the TLS stream. The logic is similar to the writer of the * @c boost::beast::http::buffer_body. * * On the every call, it will free up the buffer range that has previously been written, * then return a buffer containing data the has become available in the meantime. Otherwise, * if there is more data expected in the future, for example because a corresponding reader * has not yet finished filling the body, a `need_buffer` error is returned, to inform the * serializer to abort writing for now, which in turn leads to the outer call to * `http::async_write` to call their completion handlers with a `need_buffer` error, to * notify that more data is required for another call to `http::async_write`. */ class writer { public: using const_buffers_type = typename decltype(value_type::buffer)::const_buffers_type; template explicit writer(const boost::beast::http::header& h, value_type& b) : body(b) { } /** * This constructor is needed specifically for boost-1.66, which was the first version * the beast library was introduced and is still used on older (supported) distros. */ template explicit writer(const boost::beast::http::message& msg) : body(const_cast(msg.body())) { } void init(boost::beast::error_code& ec) { ec = {}; } boost::optional> get(boost::beast::error_code& ec) { using namespace boost::beast::http; if (sizeWritten > 0) { body.buffer.consume(std::exchange(sizeWritten, 0)); } if (body.buffer.size()) { ec = {}; sizeWritten = body.buffer.size(); return {{body.buffer.data(), body.more}}; } if (body.more) { ec = {make_error_code(error::need_buffer)}; } else { ec = {}; } return boost::none; } private: value_type& body; std::size_t sizeWritten = 0; }; }; /** * A wrapper class for a boost::beast HTTP request * * @ingroup remote */ class HttpRequest: public boost::beast::http::request { public: using ParserType = boost::beast::http::request_parser; HttpRequest(Shared::Ptr stream); /** * Parse the header of the repsonse using the internal parser object. * * This first performs an @f async_read_header() into the parser, then copies * the parsed header into this object. */ void ParseHeader(boost::beast::flat_buffer& buf, boost::asio::yield_context yc); /** * Parse the body of the repsonse using the internal parser object. * * This first performs an async_read() into the parser, then moves the parsed body * into this object. * * @param buf The buffer used to track the state of the connection * @param yc The yield_context for this operation */ void ParseBody(boost::beast::flat_buffer& buf, boost::asio::yield_context yc); ParserType& Parser() { return m_Parser; } const ApiUser::Ptr& User(); void User(const ApiUser::Ptr& user); const icinga::Url::Ptr& Url(); const Dictionary::Ptr& Params() const; void DecodeParams(); Value GetLastParameter(const String& key) const; /** * Return true if pretty printing was requested. */ bool IsPretty() const; /** * Return true if a verbose response was requested. */ bool IsVerbose() const; private: ApiUser::Ptr m_User; Url::Ptr m_Url; Dictionary::Ptr m_Params; ParserType m_Parser; Shared::Ptr m_Stream; }; /** * A wrapper class for a boost::beast HTTP response * * @ingroup remote */ class HttpResponse: public boost::beast::http::response> { public: using string_view = boost::beast::string_view; HttpResponse(Shared::Ptr stream); /** * Writes as much of the response as is currently available. * * Uses chunk-encoding if the content_length has not been set by the time this is called * for the first time. * * The caller needs to ensure that the header is finished before calling this for the * first time as changes to the header afterwards will not have any effect. * * @param yc The yield_context for this operation */ void Write(boost::asio::yield_context yc); bool IsWritable() const { return m_Stream->lowest_layer().is_open(); } /** * Resets the serializer of this message, so that it can be changed and written again. */ void Reset(); /** * Enables chunked encoding. */ void StartStreaming(); void SendJsonBody(const Value& val, bool pretty = false); void SendJsonError(const int code, String info = String(), String diagInfo = String(), bool pretty = false, bool verbose = false); void SendJsonError(const Dictionary::Ptr& params, const int code, String info = String(), String diagInfo = String()); private: using Serializer = boost::beast::http::response_serializer; std::unique_ptr m_Serializer{new Serializer{*this}}; bool m_HeaderDone = false; Shared::Ptr m_Stream; }; } #endif /* HTTPUTILITY_H */