mirror of
https://github.com/Icinga/icinga2.git
synced 2025-07-27 07:34:15 +02:00
Add HttpRequest and HttpResponse classes
This commit is contained in:
parent
1f15f0ff07
commit
71d490e0cc
@ -27,6 +27,7 @@ set(remote_SOURCES
|
|||||||
eventshandler.cpp eventshandler.hpp
|
eventshandler.cpp eventshandler.hpp
|
||||||
filterutility.cpp filterutility.hpp
|
filterutility.cpp filterutility.hpp
|
||||||
httphandler.cpp httphandler.hpp
|
httphandler.cpp httphandler.hpp
|
||||||
|
httpmessage.cpp httpmessage.hpp
|
||||||
httpserverconnection.cpp httpserverconnection.hpp
|
httpserverconnection.cpp httpserverconnection.hpp
|
||||||
httputility.cpp httputility.hpp
|
httputility.cpp httputility.hpp
|
||||||
infohandler.cpp infohandler.hpp
|
infohandler.cpp infohandler.hpp
|
||||||
|
161
lib/remote/httpmessage.cpp
Normal file
161
lib/remote/httpmessage.cpp
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
#include "remote/httpmessage.hpp"
|
||||||
|
#include "remote/httputility.hpp"
|
||||||
|
#include "remote/url.hpp"
|
||||||
|
#include "base/json.hpp"
|
||||||
|
#include "base/logger.hpp"
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <boost/beast/http.hpp>
|
||||||
|
|
||||||
|
using namespace icinga;
|
||||||
|
|
||||||
|
HttpRequest::HttpRequest(Shared<AsioTlsStream>::Ptr stream)
|
||||||
|
: m_Stream(std::forward<decltype(m_Stream)>(stream)){}
|
||||||
|
|
||||||
|
void HttpRequest::ParseHeader(boost::beast::flat_buffer & buf, boost::asio::yield_context yc)
|
||||||
|
{
|
||||||
|
boost::beast::http::async_read_header(*m_Stream, buf, m_Parser, yc);
|
||||||
|
base() = m_Parser.get().base();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpRequest::ParseBody(boost::beast::flat_buffer & buf, boost::asio::yield_context yc)
|
||||||
|
{
|
||||||
|
boost::beast::http::async_read(*m_Stream, buf, m_Parser, yc);
|
||||||
|
body() = std::move(m_Parser.release().body());
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApiUser::Ptr& HttpRequest::User()
|
||||||
|
{
|
||||||
|
return m_User;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpRequest::User(const ApiUser::Ptr& user)
|
||||||
|
{
|
||||||
|
m_User = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Url::Ptr& HttpRequest::Url()
|
||||||
|
{
|
||||||
|
if (!m_Url) {
|
||||||
|
m_Url = new icinga::Url(std::string(target()));
|
||||||
|
}
|
||||||
|
return m_Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dictionary::Ptr& HttpRequest::Params() const
|
||||||
|
{
|
||||||
|
return m_Params;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpRequest::DecodeParams()
|
||||||
|
{
|
||||||
|
if (!body().empty()) {
|
||||||
|
Log(LogDebug, "HttpUtility")
|
||||||
|
<< "Request body: '" << body() << '\'';
|
||||||
|
|
||||||
|
m_Params = JsonDecode(body());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_Params)
|
||||||
|
m_Params = new Dictionary();
|
||||||
|
|
||||||
|
std::map<String, ArrayData> query;
|
||||||
|
for (const auto& kv : Url()->GetQuery()) {
|
||||||
|
query[kv.first].emplace_back(kv.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [key, val] : query) {
|
||||||
|
m_Params->Set(key, new Array{std::move(val)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Value HttpRequest::GetLastParameter(const String& key) const
|
||||||
|
{
|
||||||
|
return HttpUtility::GetLastParameter(Params(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpRequest::IsPretty() const
|
||||||
|
{
|
||||||
|
return GetLastParameter("pretty");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpRequest::IsVerbose() const
|
||||||
|
{
|
||||||
|
return GetLastParameter("verbose");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::HttpResponse(Shared<AsioTlsStream>::Ptr stream)
|
||||||
|
: m_Stream(std::move(stream))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpResponse::Write(boost::asio::yield_context yc)
|
||||||
|
{
|
||||||
|
if (!chunked()) {
|
||||||
|
ASSERT(!m_Serializer->is_header_done());
|
||||||
|
prepare_payload();
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::system::error_code ec;
|
||||||
|
boost::beast::http::async_write(*m_Stream, *m_Serializer, yc[ec]);
|
||||||
|
if (ec && ec != boost::beast::http::error::need_buffer) {
|
||||||
|
if (!yc.ec_) {
|
||||||
|
BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
|
||||||
|
} else {
|
||||||
|
*yc.ec_ = ec;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_Stream->async_flush(yc);
|
||||||
|
|
||||||
|
ASSERT(chunked() || m_Serializer->is_done());
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpResponse::Reset()
|
||||||
|
{
|
||||||
|
m_Serializer.reset(new Serializer{*this});
|
||||||
|
content_length(boost::none);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpResponse::StartStreaming()
|
||||||
|
{
|
||||||
|
ASSERT(body().Size() == 0 && !m_Serializer->is_header_done());
|
||||||
|
body().Begin();
|
||||||
|
chunked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpResponse::SendJsonBody(const Value& val, bool pretty)
|
||||||
|
{
|
||||||
|
namespace http = boost::beast::http;
|
||||||
|
|
||||||
|
set(http::field::content_type, "application/json");
|
||||||
|
auto adapter = std::make_shared<decltype(BeastHttpMessageAdapter(*this))>(*this);
|
||||||
|
JsonEncoder encoder(adapter, pretty);
|
||||||
|
encoder.Encode(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpResponse::SendJsonError(int code, String info, String diagInfo, bool pretty, bool verbose)
|
||||||
|
{
|
||||||
|
Dictionary::Ptr response = new Dictionary({ { "error", code } });
|
||||||
|
|
||||||
|
if (!info.IsEmpty()) {
|
||||||
|
response->Set("status", std::move(info));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose && !diagInfo.IsEmpty()) {
|
||||||
|
response->Set("diagnostic_information", std::move(diagInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
result(code);
|
||||||
|
|
||||||
|
SendJsonBody(response, pretty);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpResponse::SendJsonError(const Dictionary::Ptr& params, int code, String info, String diagInfo)
|
||||||
|
{
|
||||||
|
bool verbose = params && HttpUtility::GetLastParameter(params, "verbose");
|
||||||
|
bool pretty = params && HttpUtility::GetLastParameter(params, "pretty");
|
||||||
|
SendJsonError(code, std::move(info), std::move(diagInfo), pretty, verbose);
|
||||||
|
}
|
317
lib/remote/httpmessage.hpp
Normal file
317
lib/remote/httpmessage.hpp
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
/* 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 <boost/beast/http.hpp>
|
||||||
|
#include <boost/version.hpp>
|
||||||
|
|
||||||
|
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<class DynamicBuffer>
|
||||||
|
struct SerializableBody
|
||||||
|
{
|
||||||
|
class reader;
|
||||||
|
class writer;
|
||||||
|
|
||||||
|
class value_type
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
template <typename T>
|
||||||
|
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<T>(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 <bool isRequest, class Fields>
|
||||||
|
explicit reader(boost::beast::http::header<isRequest, Fields>& h, value_type& b) : body(b)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(const boost::optional<std::uint64_t>& n, boost::beast::error_code& ec)
|
||||||
|
{
|
||||||
|
ec = {};
|
||||||
|
body.Begin();
|
||||||
|
#if BOOST_VERSION >= 107000
|
||||||
|
if (n) {
|
||||||
|
body.buffer.reserve(*n);
|
||||||
|
}
|
||||||
|
#endif /* BOOST_VERSION */
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class ConstBufferSequence>
|
||||||
|
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 <bool isRequest, class Fields>
|
||||||
|
explicit writer(const boost::beast::http::header<isRequest, Fields>& 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 <bool isRequest, class Fields>
|
||||||
|
explicit writer(const boost::beast::http::message<isRequest, SerializableBody, Fields>& msg)
|
||||||
|
: body(const_cast<value_type &>(msg.body()))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(boost::beast::error_code& ec) { ec = {}; }
|
||||||
|
|
||||||
|
boost::optional<std::pair<const_buffers_type, bool>> 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<boost::beast::http::string_body>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ParserType = boost::beast::http::request_parser<body_type>;
|
||||||
|
|
||||||
|
HttpRequest(Shared<AsioTlsStream>::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<AsioTlsStream>::Ptr m_Stream;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper class for a boost::beast HTTP response
|
||||||
|
*
|
||||||
|
* @ingroup remote
|
||||||
|
*/
|
||||||
|
class HttpResponse: public boost::beast::http::response<SerializableBody<boost::beast::multi_buffer>>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using string_view = boost::beast::string_view;
|
||||||
|
HttpResponse(Shared<AsioTlsStream>::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<HttpResponse::body_type>;
|
||||||
|
std::unique_ptr<Serializer> m_Serializer{new Serializer{*this}};
|
||||||
|
bool m_HeaderDone = false;
|
||||||
|
|
||||||
|
Shared<AsioTlsStream>::Ptr m_Stream;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* HTTPUTILITY_H */
|
Loading…
x
Reference in New Issue
Block a user