mirror of
https://github.com/Icinga/icinga2.git
synced 2026-02-03 20:40:17 -05:00
Merge pull request #10516 from Icinga/http-handlers-stream-refactor
Refactor HTTP connection handling and some handlers to stream responses
This commit is contained in:
commit
87df80d322
50 changed files with 2301 additions and 609 deletions
|
|
@ -27,6 +27,7 @@ set(remote_SOURCES
|
|||
eventshandler.cpp eventshandler.hpp
|
||||
filterutility.cpp filterutility.hpp
|
||||
httphandler.cpp httphandler.hpp
|
||||
httpmessage.cpp httpmessage.hpp
|
||||
httpserverconnection.cpp httpserverconnection.hpp
|
||||
httputility.cpp httputility.hpp
|
||||
infohandler.cpp infohandler.hpp
|
||||
|
|
|
|||
|
|
@ -17,17 +17,15 @@ REGISTER_URLHANDLER("/v1/actions", ActionsHandler);
|
|||
|
||||
bool ActionsHandler::HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() != 3)
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -17,14 +17,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -162,12 +162,12 @@ public:
|
|||
return m_WaitGroup;
|
||||
}
|
||||
|
||||
protected:
|
||||
void OnConfigLoaded() override;
|
||||
void OnAllConfigLoaded() override;
|
||||
void Start(bool runtimeCreated) override;
|
||||
void Stop(bool runtimeDeleted) override;
|
||||
|
||||
protected:
|
||||
void ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
|
||||
void ValidateTlsHandshakeTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) override;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,18 +15,17 @@ REGISTER_URLHANDLER("/v1/config/files", ConfigFilesHandler);
|
|||
|
||||
bool ConfigFilesHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (request.method() != http::verb::get)
|
||||
return false;
|
||||
|
||||
|
|
@ -78,14 +77,9 @@ bool ConfigFilesHandler::HandleRequest(
|
|||
}
|
||||
|
||||
try {
|
||||
std::ifstream fp(path.CStr(), std::ifstream::in | std::ifstream::binary);
|
||||
fp.exceptions(std::ifstream::badbit);
|
||||
|
||||
String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
|
||||
response.result(http::status::ok);
|
||||
response.set(http::field::content_type, "application/octet-stream");
|
||||
response.body() = content;
|
||||
response.content_length(response.body().size());
|
||||
response.SendFile(path, yc);
|
||||
} catch (const std::exception& ex) {
|
||||
HttpUtility::SendJsonError(response, params, 500, "Could not read file.",
|
||||
DiagnosticInformation(ex));
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,43 +13,40 @@ REGISTER_URLHANDLER("/v1/config/packages", ConfigPackagesHandler);
|
|||
|
||||
bool ConfigPackagesHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() > 4)
|
||||
return false;
|
||||
|
||||
if (request.method() == http::verb::get)
|
||||
HandleGet(user, request, url, response, params);
|
||||
HandleGet(request, response);
|
||||
else if (request.method() == http::verb::post)
|
||||
HandlePost(user, request, url, response, params);
|
||||
HandlePost(request, response);
|
||||
else if (request.method() == http::verb::delete_)
|
||||
HandleDelete(user, request, url, response, params);
|
||||
HandleDelete(request, response);
|
||||
else
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConfigPackagesHandler::HandleGet(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
)
|
||||
void ConfigPackagesHandler::HandleGet(const HttpRequest& request, HttpResponse& response)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
FilterUtility::CheckPermission(user, "config/query");
|
||||
|
||||
std::vector<String> packages;
|
||||
|
|
@ -90,16 +87,14 @@ void ConfigPackagesHandler::HandleGet(
|
|||
HttpUtility::SendJsonBody(response, params, result);
|
||||
}
|
||||
|
||||
void ConfigPackagesHandler::HandlePost(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
)
|
||||
void ConfigPackagesHandler::HandlePost(const HttpRequest& request, HttpResponse& response)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
FilterUtility::CheckPermission(user, "config/modify");
|
||||
|
||||
if (url->GetPath().size() >= 4)
|
||||
|
|
@ -142,16 +137,14 @@ void ConfigPackagesHandler::HandlePost(
|
|||
HttpUtility::SendJsonBody(response, params, result);
|
||||
}
|
||||
|
||||
void ConfigPackagesHandler::HandleDelete(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
)
|
||||
void ConfigPackagesHandler::HandleDelete(const HttpRequest& request, HttpResponse& response)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
FilterUtility::CheckPermission(user, "config/modify");
|
||||
|
||||
if (url->GetPath().size() >= 4)
|
||||
|
|
|
|||
|
|
@ -15,38 +15,15 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
|
||||
private:
|
||||
void HandleGet(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
);
|
||||
void HandlePost(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
);
|
||||
void HandleDelete(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
);
|
||||
void HandleGet(const HttpRequest& request, HttpResponse& response);
|
||||
void HandlePost(const HttpRequest& request, HttpResponse& response);
|
||||
void HandleDelete(const HttpRequest& request, HttpResponse& response);
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,43 +20,40 @@ static std::mutex l_RunningPackageUpdatesMutex; // Protects the above two variab
|
|||
|
||||
bool ConfigStagesHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() > 5)
|
||||
return false;
|
||||
|
||||
if (request.method() == http::verb::get)
|
||||
HandleGet(user, request, url, response, params);
|
||||
HandleGet(request, response);
|
||||
else if (request.method() == http::verb::post)
|
||||
HandlePost(user, request, url, response, params);
|
||||
HandlePost(request, response);
|
||||
else if (request.method() == http::verb::delete_)
|
||||
HandleDelete(user, request, url, response, params);
|
||||
HandleDelete(request, response);
|
||||
else
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConfigStagesHandler::HandleGet(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
)
|
||||
void ConfigStagesHandler::HandleGet(const HttpRequest& request, HttpResponse& response)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
FilterUtility::CheckPermission(user, "config/query");
|
||||
|
||||
if (url->GetPath().size() >= 4)
|
||||
|
|
@ -95,16 +92,14 @@ void ConfigStagesHandler::HandleGet(
|
|||
HttpUtility::SendJsonBody(response, params, result);
|
||||
}
|
||||
|
||||
void ConfigStagesHandler::HandlePost(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
)
|
||||
void ConfigStagesHandler::HandlePost(const HttpRequest& request, HttpResponse& response)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
FilterUtility::CheckPermission(user, "config/modify");
|
||||
|
||||
if (url->GetPath().size() >= 4)
|
||||
|
|
@ -208,16 +203,14 @@ void ConfigStagesHandler::HandlePost(
|
|||
HttpUtility::SendJsonBody(response, params, result);
|
||||
}
|
||||
|
||||
void ConfigStagesHandler::HandleDelete(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
)
|
||||
void ConfigStagesHandler::HandleDelete(const HttpRequest& request, HttpResponse& response)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
FilterUtility::CheckPermission(user, "config/modify");
|
||||
|
||||
if (url->GetPath().size() >= 4)
|
||||
|
|
|
|||
|
|
@ -15,38 +15,15 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
|
||||
private:
|
||||
void HandleGet(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
);
|
||||
void HandlePost(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
);
|
||||
void HandleDelete(
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params
|
||||
);
|
||||
void HandleGet(const HttpRequest& request, HttpResponse& response);
|
||||
void HandlePost(const HttpRequest& request, HttpResponse& response);
|
||||
void HandleDelete(const HttpRequest& request, HttpResponse& response);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,18 +55,17 @@ static void EnsureFrameCleanupTimer()
|
|||
|
||||
bool ConsoleHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() != 3)
|
||||
return false;
|
||||
|
||||
|
|
@ -96,17 +95,16 @@ bool ConsoleHandler::HandleRequest(
|
|||
}
|
||||
|
||||
if (methodName == "execute-script")
|
||||
return ExecuteScriptHelper(request, response, params, command, session, sandboxed);
|
||||
return ExecuteScriptHelper(request, response, command, session, sandboxed);
|
||||
else if (methodName == "auto-complete-script")
|
||||
return AutocompleteScriptHelper(request, response, params, command, session, sandboxed);
|
||||
return AutocompleteScriptHelper(request, response, command, session, sandboxed);
|
||||
|
||||
HttpUtility::SendJsonError(response, params, 400, "Invalid method specified: " + methodName);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConsoleHandler::ExecuteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
|
||||
bool ConsoleHandler::ExecuteScriptHelper(const HttpRequest& request, HttpResponse& response,
|
||||
const String& command, const String& session, bool sandboxed)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
|
|
@ -174,14 +172,13 @@ bool ConsoleHandler::ExecuteScriptHelper(boost::beast::http::request<boost::beas
|
|||
});
|
||||
|
||||
response.result(http::status::ok);
|
||||
HttpUtility::SendJsonBody(response, params, result);
|
||||
HttpUtility::SendJsonBody(response, request.Params(), result);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConsoleHandler::AutocompleteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
|
||||
bool ConsoleHandler::AutocompleteScriptHelper(const HttpRequest& request, HttpResponse& response,
|
||||
const String& command, const String& session, bool sandboxed)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
|
|
@ -213,7 +210,7 @@ bool ConsoleHandler::AutocompleteScriptHelper(boost::beast::http::request<boost:
|
|||
});
|
||||
|
||||
response.result(http::status::ok);
|
||||
HttpUtility::SendJsonBody(response, params, result);
|
||||
HttpUtility::SendJsonBody(response, request.Params(), result);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,25 +24,18 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
|
||||
static std::vector<String> GetAutocompletionSuggestions(const String& word, ScriptFrame& frame);
|
||||
|
||||
private:
|
||||
static bool ExecuteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed);
|
||||
static bool AutocompleteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed);
|
||||
static bool ExecuteScriptHelper(const HttpRequest& request, HttpResponse& response,
|
||||
const String& command, const String& session, bool sandboxed);
|
||||
static bool AutocompleteScriptHelper(const HttpRequest& request, HttpResponse& response,
|
||||
const String& command, const String& session, bool sandboxed);
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,18 +17,17 @@ REGISTER_URLHANDLER("/v1/objects", CreateObjectHandler);
|
|||
|
||||
bool CreateObjectHandler::HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() != 4)
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,18 +17,17 @@ REGISTER_URLHANDLER("/v1/objects", DeleteObjectHandler);
|
|||
|
||||
bool DeleteObjectHandler::HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -41,19 +41,18 @@ const String l_ApiQuery ("<API query>");
|
|||
|
||||
bool EventsHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace asio = boost::asio;
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() != 2)
|
||||
return false;
|
||||
|
||||
|
|
@ -101,33 +100,27 @@ bool EventsHandler::HandleRequest(
|
|||
|
||||
EventsSubscriber subscriber (std::move(eventTypes), HttpUtility::GetLastParameter(params, "filter"), l_ApiQuery);
|
||||
|
||||
server.StartStreaming();
|
||||
IoBoundWorkSlot dontLockTheIoThread (yc);
|
||||
|
||||
response.result(http::status::ok);
|
||||
response.set(http::field::content_type, "application/json");
|
||||
response.StartStreaming(true);
|
||||
// Send response headers before waiting for the first event.
|
||||
response.Flush(yc);
|
||||
|
||||
IoBoundWorkSlot dontLockTheIoThread (yc);
|
||||
|
||||
http::async_write(stream, response, yc);
|
||||
stream.async_flush(yc);
|
||||
|
||||
asio::const_buffer newLine ("\n", 1);
|
||||
auto encoder = response.GetJsonEncoder();
|
||||
|
||||
for (;;) {
|
||||
auto event (subscriber.GetInbox()->Shift(yc));
|
||||
|
||||
if (event) {
|
||||
String body = JsonEncode(event);
|
||||
|
||||
boost::algorithm::replace_all(body, "\n", "");
|
||||
|
||||
asio::const_buffer payload (body.CStr(), body.GetLength());
|
||||
|
||||
asio::async_write(stream, payload, yc);
|
||||
asio::async_write(stream, newLine, yc);
|
||||
stream.async_flush(yc);
|
||||
} else if (server.Disconnected()) {
|
||||
if (response.IsClientDisconnected()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event) {
|
||||
encoder.Encode(event);
|
||||
response.body() << '\n';
|
||||
response.Flush(yc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,14 +16,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -48,19 +48,16 @@ void HttpHandler::Register(const Url::Ptr& url, const HttpHandler::Ptr& handler)
|
|||
|
||||
void HttpHandler::ProcessRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
Dictionary::Ptr node = m_UrlTree;
|
||||
std::vector<HttpHandler::Ptr> handlers;
|
||||
|
||||
Url::Ptr url = new Url(std::string(request.target()));
|
||||
auto& path (url->GetPath());
|
||||
request.DecodeUrl();
|
||||
auto& path (request.Url()->GetPath());
|
||||
|
||||
for (std::vector<String>::size_type i = 0; i <= path.size(); i++) {
|
||||
Array::Ptr current_handlers = node->Get("handlers");
|
||||
|
|
@ -90,12 +87,10 @@ void HttpHandler::ProcessRequest(
|
|||
|
||||
std::reverse(handlers.begin(), handlers.end());
|
||||
|
||||
Dictionary::Ptr params;
|
||||
|
||||
try {
|
||||
params = HttpUtility::FetchRequestParameters(url, request.body());
|
||||
request.DecodeParams();
|
||||
} catch (const std::exception& ex) {
|
||||
HttpUtility::SendJsonError(response, params, 400, "Invalid request body: " + DiagnosticInformation(ex, false));
|
||||
HttpUtility::SendJsonError(response, request.Params(), 400, "Invalid request body: " + DiagnosticInformation(ex, false));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -109,12 +104,25 @@ void HttpHandler::ProcessRequest(
|
|||
*/
|
||||
try {
|
||||
for (const HttpHandler::Ptr& handler : handlers) {
|
||||
if (handler->HandleRequest(waitGroup, stream, user, request, url, response, params, yc, server)) {
|
||||
if (handler->HandleRequest(waitGroup, request, response, yc)) {
|
||||
processed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
// Errors related to writing the response should be handled in HttpServerConnection.
|
||||
if (dynamic_cast<const boost::system::system_error*>(&ex)) {
|
||||
throw;
|
||||
}
|
||||
|
||||
/* This means we can't send an error response because the exception was thrown
|
||||
* in the middle of a streaming response. We can't send any error response, so the
|
||||
* only thing we can do is propagate it up.
|
||||
*/
|
||||
if (response.HasSerializationStarted()) {
|
||||
throw;
|
||||
}
|
||||
|
||||
Log(LogWarning, "HttpServerConnection")
|
||||
<< "Error while processing HTTP request: " << ex.what();
|
||||
|
||||
|
|
@ -122,7 +130,7 @@ void HttpHandler::ProcessRequest(
|
|||
}
|
||||
|
||||
if (!processed) {
|
||||
HttpUtility::SendJsonError(response, params, 404, "The requested path '" + boost::algorithm::join(path, "/") +
|
||||
HttpUtility::SendJsonError(response, request.Params(), 404, "The requested path '" + boost::algorithm::join(path, "/") +
|
||||
"' could not be found or the request method is not valid for this path.");
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@
|
|||
#define HTTPHANDLER_H
|
||||
|
||||
#include "remote/i2-remote.hpp"
|
||||
#include "base/io-engine.hpp"
|
||||
#include "remote/url.hpp"
|
||||
#include "remote/httpserverconnection.hpp"
|
||||
#include "remote/httpmessage.hpp"
|
||||
#include "remote/apiuser.hpp"
|
||||
#include "base/registry.hpp"
|
||||
#include "base/tlsstream.hpp"
|
||||
|
|
@ -28,25 +30,17 @@ public:
|
|||
|
||||
virtual bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) = 0;
|
||||
|
||||
static void Register(const Url::Ptr& url, const HttpHandler::Ptr& handler);
|
||||
static void ProcessRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
);
|
||||
|
||||
private:
|
||||
|
|
|
|||
196
lib/remote/httpmessage.cpp
Normal file
196
lib/remote/httpmessage.cpp
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#include "remote/httpmessage.hpp"
|
||||
#include "base/io-engine.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
|
||||
*/
|
||||
class HttpResponseJsonWriter : public AsyncJsonWriter
|
||||
{
|
||||
public:
|
||||
explicit HttpResponseJsonWriter(HttpResponse& 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:
|
||||
HttpResponse& m_Message;
|
||||
};
|
||||
|
||||
HttpRequest::HttpRequest(Shared<AsioTlsStream>::Ptr stream) : m_Stream(std::move(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());
|
||||
}
|
||||
|
||||
ApiUser::Ptr HttpRequest::User() const
|
||||
{
|
||||
return m_User;
|
||||
}
|
||||
|
||||
void HttpRequest::User(const ApiUser::Ptr& user)
|
||||
{
|
||||
m_User = user;
|
||||
}
|
||||
|
||||
Url::Ptr HttpRequest::Url() const
|
||||
{
|
||||
return m_Url;
|
||||
}
|
||||
|
||||
void HttpRequest::DecodeUrl()
|
||||
{
|
||||
m_Url = new icinga::Url(std::string(target()));
|
||||
}
|
||||
|
||||
Dictionary::Ptr HttpRequest::Params() const
|
||||
{
|
||||
return m_Params;
|
||||
}
|
||||
|
||||
void HttpRequest::DecodeParams()
|
||||
{
|
||||
if (!m_Url) {
|
||||
DecodeUrl();
|
||||
}
|
||||
m_Params = HttpUtility::FetchRequestParameters(m_Url, body());
|
||||
}
|
||||
|
||||
HttpResponse::HttpResponse(Shared<AsioTlsStream>::Ptr stream, HttpServerConnection::Ptr server)
|
||||
: m_Server(std::move(server)), m_Stream(std::move(stream))
|
||||
{
|
||||
}
|
||||
|
||||
void HttpResponse::Clear()
|
||||
{
|
||||
ASSERT(!m_SerializationStarted);
|
||||
boost::beast::http::response<body_type>::operator=({});
|
||||
}
|
||||
|
||||
void HttpResponse::Flush(boost::asio::yield_context yc)
|
||||
{
|
||||
if (!chunked() && !has_content_length()) {
|
||||
ASSERT(!m_SerializationStarted);
|
||||
prepare_payload();
|
||||
}
|
||||
|
||||
m_SerializationStarted = true;
|
||||
|
||||
if (!m_Serializer.is_header_done()) {
|
||||
boost::beast::http::write_header(*m_Stream, m_Serializer);
|
||||
}
|
||||
|
||||
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_) {
|
||||
*yc.ec_ = ec;
|
||||
return;
|
||||
}
|
||||
BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
|
||||
}
|
||||
m_Stream->async_flush(yc);
|
||||
|
||||
ASSERT(m_Serializer.is_done() || !body().Finished());
|
||||
}
|
||||
|
||||
void HttpResponse::StartStreaming(bool checkForDisconnect)
|
||||
{
|
||||
ASSERT(body().Size() == 0 && !m_SerializationStarted);
|
||||
body().Start();
|
||||
chunked(true);
|
||||
|
||||
if (checkForDisconnect) {
|
||||
ASSERT(m_Server);
|
||||
m_Server->StartDetectClientSideShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpResponse::IsClientDisconnected() const
|
||||
{
|
||||
ASSERT(m_Server);
|
||||
return m_Server->Disconnected();
|
||||
}
|
||||
|
||||
void HttpResponse::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);
|
||||
|
||||
content_length(remaining);
|
||||
body().Start();
|
||||
|
||||
while (remaining) {
|
||||
auto maxTransfer = std::min(remaining, static_cast<std::uint64_t>(l_FlushThreshold));
|
||||
|
||||
auto buf = *body().Buffer().prepare(maxTransfer).begin();
|
||||
fp.read(static_cast<char*>(buf.data()), buf.size());
|
||||
body().Buffer().commit(buf.size());
|
||||
|
||||
remaining -= buf.size();
|
||||
Flush(yc);
|
||||
}
|
||||
}
|
||||
|
||||
JsonEncoder HttpResponse::GetJsonEncoder(bool pretty)
|
||||
{
|
||||
return JsonEncoder{std::make_shared<HttpResponseJsonWriter>(*this), pretty};
|
||||
}
|
||||
281
lib/remote/httpmessage.hpp
Normal file
281
lib/remote/httpmessage.hpp
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "base/dictionary.hpp"
|
||||
#include "base/json.hpp"
|
||||
#include "base/tlsstream.hpp"
|
||||
#include "remote/apiuser.hpp"
|
||||
#include "remote/httpserverconnection.hpp"
|
||||
#include "remote/url.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 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(m_Buffer) << std::forward<T>(right);
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t Size() const { return m_Buffer.size(); }
|
||||
|
||||
void Finish() { m_More = false; }
|
||||
bool Finished() { return !m_More; }
|
||||
void Start() { m_More = true; }
|
||||
DynamicBuffer& Buffer() { return m_Buffer; }
|
||||
|
||||
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 m_More = false;
|
||||
DynamicBuffer m_Buffer;
|
||||
};
|
||||
|
||||
static std::uint64_t size(const value_type& body) { return body.Size(); }
|
||||
|
||||
/**
|
||||
* 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 DynamicBuffer::const_buffers_type;
|
||||
|
||||
#if BOOST_VERSION > 106600
|
||||
template<bool isRequest, class Fields>
|
||||
explicit writer(const boost::beast::http::header<isRequest, Fields>&, value_type& b) : m_Body(b)
|
||||
{
|
||||
}
|
||||
#else
|
||||
/**
|
||||
* 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)
|
||||
: m_Body(const_cast<value_type&>(msg.body()))
|
||||
{
|
||||
}
|
||||
#endif
|
||||
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 (m_SizeWritten > 0) {
|
||||
m_Body.m_Buffer.consume(std::exchange(m_SizeWritten, 0));
|
||||
}
|
||||
|
||||
if (m_Body.m_Buffer.size()) {
|
||||
ec = {};
|
||||
m_SizeWritten = m_Body.m_Buffer.size();
|
||||
return {{m_Body.m_Buffer.data(), m_Body.m_More}};
|
||||
}
|
||||
|
||||
if (m_Body.m_More) {
|
||||
ec = {make_error_code(error::need_buffer)};
|
||||
} else {
|
||||
ec = {};
|
||||
}
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
private:
|
||||
value_type& m_Body;
|
||||
std::size_t m_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>;
|
||||
|
||||
explicit HttpRequest(Shared<AsioTlsStream>::Ptr stream);
|
||||
|
||||
/**
|
||||
* Parse the header of the response 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 response 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; }
|
||||
|
||||
[[nodiscard]] ApiUser::Ptr User() const;
|
||||
void User(const ApiUser::Ptr& user);
|
||||
|
||||
[[nodiscard]] icinga::Url::Ptr Url() const;
|
||||
void DecodeUrl();
|
||||
|
||||
[[nodiscard]] Dictionary::Ptr Params() const;
|
||||
void DecodeParams();
|
||||
|
||||
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:
|
||||
explicit HttpResponse(Shared<AsioTlsStream>::Ptr stream, HttpServerConnection::Ptr server = nullptr);
|
||||
|
||||
/* Delete the base class clear() which is inherited from the fields<> class and doesn't
|
||||
* clear things like the body or obviously our own members.
|
||||
*/
|
||||
void clear() = delete;
|
||||
|
||||
/**
|
||||
* Clear the header and body of the message.
|
||||
*
|
||||
* @note This can only be used when nothing has been written to the stream yet.
|
||||
*/
|
||||
void Clear();
|
||||
|
||||
/**
|
||||
* 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 Flush(boost::asio::yield_context yc);
|
||||
|
||||
[[nodiscard]] bool HasSerializationStarted() const { return m_SerializationStarted; }
|
||||
|
||||
/**
|
||||
* Enables chunked encoding.
|
||||
*
|
||||
* Optionally starts a coroutine that reads from the stream and checks for client-side
|
||||
* disconnects. In this case, the stream can not be reused after the response has been
|
||||
* sent and any further requests sent over the connections will be discarded, even if
|
||||
* no client-side disconnect occurs. This requires that this object has been constructed
|
||||
* with a valid HttpServerConnection::Ptr.
|
||||
*
|
||||
* @param checkForDisconnect Whether to start a coroutine to detect disconnects
|
||||
*/
|
||||
void StartStreaming(bool checkForDisconnect = false);
|
||||
|
||||
/**
|
||||
* Check if the server has initiated a disconnect.
|
||||
*
|
||||
* @note This requires that the message has been constructed with a pointer to the
|
||||
* @c HttpServerConnection.
|
||||
*/
|
||||
[[nodiscard]] bool IsClientDisconnected() const;
|
||||
|
||||
/**
|
||||
* Sends the contents of a file.
|
||||
*
|
||||
* This does not use chunked encoding because the file size is expected to be fixed.
|
||||
* The message will be flushed to the stream after a certain amount has been loaded into
|
||||
* the buffer.
|
||||
*
|
||||
* @todo Switch the implementation to @c boost::asio::stream_file when we require >=boost-1.78.
|
||||
*
|
||||
* @param path A path to the file
|
||||
* @param yc The yield context for flushing the message.
|
||||
*/
|
||||
void SendFile(const String& path, const boost::asio::yield_context& yc);
|
||||
|
||||
JsonEncoder GetJsonEncoder(bool pretty = false);
|
||||
|
||||
private:
|
||||
using Serializer = boost::beast::http::response_serializer<HttpResponse::body_type>;
|
||||
Serializer m_Serializer{*this};
|
||||
bool m_SerializationStarted = false;
|
||||
|
||||
HttpServerConnection::Ptr m_Server;
|
||||
Shared<AsioTlsStream>::Ptr m_Stream;
|
||||
};
|
||||
|
||||
} // namespace icinga
|
||||
|
|
@ -41,8 +41,7 @@ HttpServerConnection::HttpServerConnection(const WaitGroup::Ptr& waitGroup, cons
|
|||
}
|
||||
|
||||
HttpServerConnection::HttpServerConnection(const WaitGroup::Ptr& waitGroup, const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, boost::asio::io_context& io)
|
||||
: m_WaitGroup(waitGroup), m_Stream(stream), m_Seen(Utility::GetTime()), m_IoStrand(io), m_ShuttingDown(false), m_HasStartedStreaming(false),
|
||||
m_CheckLivenessTimer(io)
|
||||
: m_WaitGroup(waitGroup), m_Stream(stream), m_Seen(Utility::GetTime()), m_IoStrand(io), m_ShuttingDown(false), m_ConnectionReusable(true), m_CheckLivenessTimer(io)
|
||||
{
|
||||
if (authenticated) {
|
||||
m_ApiUser = ApiUser::GetByClientCN(identity);
|
||||
|
|
@ -99,14 +98,40 @@ void HttpServerConnection::Disconnect(boost::asio::yield_context yc)
|
|||
}
|
||||
}
|
||||
|
||||
void HttpServerConnection::StartStreaming()
|
||||
/**
|
||||
* Starts a coroutine that continually reads from the stream to detect a disconnect from the client.
|
||||
*
|
||||
* This can be accessed inside an @c HttpHandler via the HttpResponse::StartStreaming() method by
|
||||
* passing true as the argument, expressing that disconnect detection is desired.
|
||||
*/
|
||||
void HttpServerConnection::StartDetectClientSideShutdown()
|
||||
{
|
||||
namespace asio = boost::asio;
|
||||
|
||||
m_HasStartedStreaming = true;
|
||||
m_ConnectionReusable = false;
|
||||
|
||||
HttpServerConnection::Ptr keepAlive (this);
|
||||
|
||||
/* Technically it would be possible to detect disconnects on the TCP-side by setting the
|
||||
* socket to non-blocking and then performing a read directly on the socket with the message_peek
|
||||
* flag. As the TCP FIN message will put the connection into a CLOSE_WAIT even if the kernel
|
||||
* buffer is full, this would technically be reliable way of detecting a shutdown and free
|
||||
* of side-effects.
|
||||
*
|
||||
* However, for detecting the close_notify on the SSL/TLS-side, an async_fill() would be necessary
|
||||
* when the check on the TCP level above returns that there are readable bytes (and no FIN/eof).
|
||||
* If this async_fill() then buffers more application data and not an immediate eof, we could
|
||||
* attempt to read another message before disconnecting.
|
||||
*
|
||||
* This could either be done at the level of the handlers, via the @c HttpResponse class, or
|
||||
* generally as a separate coroutine here in @c HttpServerConnection, both (mostly) side-effect
|
||||
* free and without affecting the state of the connection.
|
||||
*
|
||||
* However, due to the complexity of this approach, involving several asio operations, message
|
||||
* flags, synchronous and asynchronous operations in blocking and non-blocking mode, ioctl cmds,
|
||||
* etc., it was decided to stick with a simple reading loop, started conditionally on request by
|
||||
* the handler.
|
||||
*/
|
||||
IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
|
||||
if (!m_ShuttingDown) {
|
||||
char buf[128];
|
||||
|
|
@ -129,10 +154,9 @@ bool HttpServerConnection::Disconnected()
|
|||
|
||||
static inline
|
||||
bool EnsureValidHeaders(
|
||||
AsioTlsStream& stream,
|
||||
boost::beast::flat_buffer& buf,
|
||||
boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
bool& shuttingDown,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
|
|
@ -147,7 +171,7 @@ bool EnsureValidHeaders(
|
|||
|
||||
boost::system::error_code ec;
|
||||
|
||||
http::async_read_header(stream, buf, parser, yc[ec]);
|
||||
request.ParseHeader(buf, yc[ec]);
|
||||
|
||||
if (ec) {
|
||||
if (ec == boost::asio::error::operation_aborted)
|
||||
|
|
@ -156,7 +180,7 @@ bool EnsureValidHeaders(
|
|||
errorMsg = ec.message();
|
||||
httpError = true;
|
||||
} else {
|
||||
switch (parser.get().version()) {
|
||||
switch (request.version()) {
|
||||
case 10:
|
||||
case 11:
|
||||
break;
|
||||
|
|
@ -168,21 +192,16 @@ bool EnsureValidHeaders(
|
|||
if (!errorMsg.IsEmpty() || httpError) {
|
||||
response.result(http::status::bad_request);
|
||||
|
||||
if (!httpError && parser.get()[http::field::accept] == "application/json") {
|
||||
HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
|
||||
{ "error", 400 },
|
||||
{ "status", String("Bad Request: ") + errorMsg }
|
||||
}));
|
||||
if (!httpError && request[http::field::accept] == "application/json") {
|
||||
HttpUtility::SendJsonError(response, nullptr, 400, "Bad Request: " + errorMsg);
|
||||
} else {
|
||||
response.set(http::field::content_type, "text/html");
|
||||
response.body() = String("<h1>Bad Request</h1><p><pre>") + errorMsg + "</pre></p>";
|
||||
response.content_length(response.body().size());
|
||||
response.body() << "<h1>Bad Request</h1><p><pre>" << errorMsg << "</pre></p>";
|
||||
}
|
||||
|
||||
response.set(http::field::connection, "close");
|
||||
|
||||
http::async_write(stream, response, yc);
|
||||
stream.async_flush(yc);
|
||||
response.Flush(yc);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -192,28 +211,24 @@ bool EnsureValidHeaders(
|
|||
|
||||
static inline
|
||||
void HandleExpect100(
|
||||
AsioTlsStream& stream,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Shared<AsioTlsStream>::Ptr& stream,
|
||||
const HttpRequest& request,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
if (request[http::field::expect] == "100-continue") {
|
||||
http::response<http::string_body> response;
|
||||
|
||||
HttpResponse response{stream};
|
||||
response.result(http::status::continue_);
|
||||
|
||||
http::async_write(stream, response, yc);
|
||||
stream.async_flush(yc);
|
||||
response.Flush(yc);
|
||||
}
|
||||
}
|
||||
|
||||
static inline
|
||||
bool HandleAccessControl(
|
||||
AsioTlsStream& stream,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
|
|
@ -240,12 +255,10 @@ bool HandleAccessControl(
|
|||
response.result(http::status::ok);
|
||||
response.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE");
|
||||
response.set(http::field::access_control_allow_headers, "Authorization, Content-Type, X-HTTP-Method-Override");
|
||||
response.body() = "Preflight OK";
|
||||
response.content_length(response.body().size());
|
||||
response.body() << "Preflight OK";
|
||||
response.set(http::field::connection, "close");
|
||||
|
||||
http::async_write(stream, response, yc);
|
||||
stream.async_flush(yc);
|
||||
response.Flush(yc);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -258,9 +271,8 @@ bool HandleAccessControl(
|
|||
|
||||
static inline
|
||||
bool EnsureAcceptHeader(
|
||||
AsioTlsStream& stream,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
|
|
@ -269,12 +281,10 @@ bool EnsureAcceptHeader(
|
|||
if (request.method() != http::verb::get && request[http::field::accept] != "application/json") {
|
||||
response.result(http::status::bad_request);
|
||||
response.set(http::field::content_type, "text/html");
|
||||
response.body() = "<h1>Accept header is missing or not set to 'application/json'.</h1>";
|
||||
response.content_length(response.body().size());
|
||||
response.body() << "<h1>Accept header is missing or not set to 'application/json'.</h1>";
|
||||
response.set(http::field::connection, "close");
|
||||
|
||||
http::async_write(stream, response, yc);
|
||||
stream.async_flush(yc);
|
||||
response.Flush(yc);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -284,16 +294,14 @@ bool EnsureAcceptHeader(
|
|||
|
||||
static inline
|
||||
bool EnsureAuthenticatedUser(
|
||||
AsioTlsStream& stream,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
ApiUser::Ptr& authenticatedUser,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
if (!authenticatedUser) {
|
||||
if (!request.User()) {
|
||||
Log(LogWarning, "HttpServerConnection")
|
||||
<< "Unauthorized request: " << request.method_string() << ' ' << request.target();
|
||||
|
||||
|
|
@ -302,18 +310,13 @@ bool EnsureAuthenticatedUser(
|
|||
response.set(http::field::connection, "close");
|
||||
|
||||
if (request[http::field::accept] == "application/json") {
|
||||
HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
|
||||
{ "error", 401 },
|
||||
{ "status", "Unauthorized. Please check your user credentials." }
|
||||
}));
|
||||
HttpUtility::SendJsonError(response, nullptr, 401, "Unauthorized. Please check your user credentials.");
|
||||
} else {
|
||||
response.set(http::field::content_type, "text/html");
|
||||
response.body() = "<h1>Unauthorized. Please check your user credentials.</h1>";
|
||||
response.content_length(response.body().size());
|
||||
response.body() << "<h1>Unauthorized. Please check your user credentials.</h1>";
|
||||
}
|
||||
|
||||
http::async_write(stream, response, yc);
|
||||
stream.async_flush(yc);
|
||||
response.Flush(yc);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -323,11 +326,9 @@ bool EnsureAuthenticatedUser(
|
|||
|
||||
static inline
|
||||
bool EnsureValidBody(
|
||||
AsioTlsStream& stream,
|
||||
boost::beast::flat_buffer& buf,
|
||||
boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
|
||||
ApiUser::Ptr& authenticatedUser,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
bool& shuttingDown,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
|
|
@ -336,7 +337,7 @@ bool EnsureValidBody(
|
|||
|
||||
{
|
||||
size_t maxSize = 1024 * 1024;
|
||||
Array::Ptr permissions = authenticatedUser->GetPermissions();
|
||||
Array::Ptr permissions = request.User()->GetPermissions();
|
||||
|
||||
if (permissions) {
|
||||
ObjectLock olock(permissions);
|
||||
|
|
@ -366,7 +367,7 @@ bool EnsureValidBody(
|
|||
}
|
||||
}
|
||||
|
||||
parser.body_limit(maxSize);
|
||||
request.Parser().body_limit(maxSize);
|
||||
}
|
||||
|
||||
if (shuttingDown)
|
||||
|
|
@ -374,7 +375,7 @@ bool EnsureValidBody(
|
|||
|
||||
boost::system::error_code ec;
|
||||
|
||||
http::async_read(stream, buf, parser, yc[ec]);
|
||||
request.ParseBody(buf, yc[ec]);
|
||||
|
||||
if (ec) {
|
||||
if (ec == boost::asio::error::operation_aborted)
|
||||
|
|
@ -389,21 +390,16 @@ bool EnsureValidBody(
|
|||
|
||||
response.result(http::status::bad_request);
|
||||
|
||||
if (parser.get()[http::field::accept] == "application/json") {
|
||||
HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
|
||||
{ "error", 400 },
|
||||
{ "status", String("Bad Request: ") + ec.message() }
|
||||
}));
|
||||
if (request[http::field::accept] == "application/json") {
|
||||
HttpUtility::SendJsonError(response, nullptr, 400, "Bad Request: " + ec.message());
|
||||
} else {
|
||||
response.set(http::field::content_type, "text/html");
|
||||
response.body() = String("<h1>Bad Request</h1><p><pre>") + ec.message() + "</pre></p>";
|
||||
response.content_length(response.body().size());
|
||||
response.body() << "<h1>Bad Request</h1><p><pre>" << ec.message() << "</pre></p>";
|
||||
}
|
||||
|
||||
response.set(http::field::connection, "close");
|
||||
|
||||
http::async_write(stream, response, yc);
|
||||
stream.async_flush(yc);
|
||||
response.Flush(yc);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -412,56 +408,34 @@ bool EnsureValidBody(
|
|||
}
|
||||
|
||||
static inline
|
||||
bool ProcessRequest(
|
||||
AsioTlsStream& stream,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
ApiUser::Ptr& authenticatedUser,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
HttpServerConnection& server,
|
||||
bool& hasStartedStreaming,
|
||||
void ProcessRequest(
|
||||
HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
std::chrono::steady_clock::duration& cpuBoundWorkTime,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
try {
|
||||
// Cache the elapsed time to acquire a CPU semaphore used to detect extremely heavy workloads.
|
||||
auto start (std::chrono::steady_clock::now());
|
||||
CpuBoundWork handlingRequest (yc);
|
||||
cpuBoundWorkTime = std::chrono::steady_clock::now() - start;
|
||||
|
||||
HttpHandler::ProcessRequest(waitGroup, stream, authenticatedUser, request, response, yc, server);
|
||||
HttpHandler::ProcessRequest(waitGroup, request, response, yc);
|
||||
response.body().Finish();
|
||||
} catch (const std::exception& ex) {
|
||||
if (hasStartedStreaming) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto sysErr (dynamic_cast<const boost::system::system_error*>(&ex));
|
||||
|
||||
if (sysErr && sysErr->code() == boost::asio::error::operation_aborted) {
|
||||
/* Since we don't know the state the stream is in, we can't send an error response and
|
||||
* have to just cause a disconnect here.
|
||||
*/
|
||||
if (response.HasSerializationStarted()) {
|
||||
throw;
|
||||
}
|
||||
|
||||
http::response<http::string_body> response;
|
||||
|
||||
HttpUtility::SendJsonError(response, nullptr, 500, "Unhandled exception" , DiagnosticInformation(ex));
|
||||
|
||||
http::async_write(stream, response, yc);
|
||||
stream.async_flush(yc);
|
||||
|
||||
return true;
|
||||
HttpUtility::SendJsonError(response, request.Params(), 500, "Unhandled exception", DiagnosticInformation(ex));
|
||||
}
|
||||
|
||||
if (hasStartedStreaming) {
|
||||
return false;
|
||||
}
|
||||
|
||||
http::async_write(stream, response, yc);
|
||||
stream.async_flush(yc);
|
||||
|
||||
return true;
|
||||
response.Flush(yc);
|
||||
}
|
||||
|
||||
void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
|
||||
|
|
@ -481,23 +455,21 @@ void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
|
|||
while (m_WaitGroup->IsLockable()) {
|
||||
m_Seen = Utility::GetTime();
|
||||
|
||||
http::parser<true, http::string_body> parser;
|
||||
http::response<http::string_body> response;
|
||||
HttpRequest request(m_Stream);
|
||||
HttpResponse response(m_Stream, this);
|
||||
|
||||
parser.header_limit(1024 * 1024);
|
||||
parser.body_limit(-1);
|
||||
request.Parser().header_limit(1024 * 1024);
|
||||
request.Parser().body_limit(-1);
|
||||
|
||||
response.set(http::field::server, l_ServerHeader);
|
||||
|
||||
if (!EnsureValidHeaders(*m_Stream, buf, parser, response, m_ShuttingDown, yc)) {
|
||||
if (!EnsureValidHeaders(buf, request, response, m_ShuttingDown, yc)) {
|
||||
break;
|
||||
}
|
||||
|
||||
m_Seen = Utility::GetTime();
|
||||
auto start (ch::steady_clock::now());
|
||||
|
||||
auto& request (parser.get());
|
||||
|
||||
{
|
||||
auto method (http::string_to_verb(request["X-Http-Method-Override"]));
|
||||
|
||||
|
|
@ -506,19 +478,19 @@ void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
|
|||
}
|
||||
}
|
||||
|
||||
HandleExpect100(*m_Stream, request, yc);
|
||||
HandleExpect100(m_Stream, request, yc);
|
||||
|
||||
auto authenticatedUser (m_ApiUser);
|
||||
|
||||
if (!authenticatedUser) {
|
||||
authenticatedUser = ApiUser::GetByAuthHeader(std::string(request[http::field::authorization]));
|
||||
if (m_ApiUser) {
|
||||
request.User(m_ApiUser);
|
||||
} else {
|
||||
request.User(ApiUser::GetByAuthHeader(std::string(request[http::field::authorization])));
|
||||
}
|
||||
|
||||
Log logMsg (LogInformation, "HttpServerConnection");
|
||||
|
||||
logMsg << "Request " << request.method_string() << ' ' << request.target()
|
||||
<< " (from " << m_PeerAddress
|
||||
<< ", user: " << (authenticatedUser ? authenticatedUser->GetName() : "<unauthenticated>")
|
||||
<< ", user: " << (request.User() ? request.User()->GetName() : "<unauthenticated>")
|
||||
<< ", agent: " << request[http::field::user_agent]; //operator[] - Returns the value for a field, or "" if it does not exist.
|
||||
|
||||
ch::steady_clock::duration cpuBoundWorkTime(0);
|
||||
|
|
@ -531,29 +503,27 @@ void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
|
|||
logMsg << " took total " << ch::duration_cast<ch::milliseconds>(ch::steady_clock::now() - start).count() << "ms.";
|
||||
});
|
||||
|
||||
if (!HandleAccessControl(*m_Stream, request, response, yc)) {
|
||||
if (!HandleAccessControl(request, response, yc)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!EnsureAcceptHeader(*m_Stream, request, response, yc)) {
|
||||
if (!EnsureAcceptHeader(request, response, yc)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!EnsureAuthenticatedUser(*m_Stream, request, authenticatedUser, response, yc)) {
|
||||
if (!EnsureAuthenticatedUser(request, response, yc)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!EnsureValidBody(*m_Stream, buf, parser, authenticatedUser, response, m_ShuttingDown, yc)) {
|
||||
if (!EnsureValidBody(buf, request, response, m_ShuttingDown, yc)) {
|
||||
break;
|
||||
}
|
||||
|
||||
m_Seen = std::numeric_limits<decltype(m_Seen)>::max();
|
||||
|
||||
if (!ProcessRequest(*m_Stream, request, authenticatedUser, response, *this, m_HasStartedStreaming, m_WaitGroup, cpuBoundWorkTime, yc)) {
|
||||
break;
|
||||
}
|
||||
ProcessRequest(request, response, m_WaitGroup, cpuBoundWorkTime, yc);
|
||||
|
||||
if (request.version() != 11 || request[http::field::connection] == "close") {
|
||||
if (!request.keep_alive() || !m_ConnectionReusable) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public:
|
|||
const Shared<AsioTlsStream>::Ptr& stream);
|
||||
|
||||
void Start();
|
||||
void StartStreaming();
|
||||
void StartDetectClientSideShutdown();
|
||||
bool Disconnected();
|
||||
|
||||
private:
|
||||
|
|
@ -41,7 +41,7 @@ private:
|
|||
String m_PeerAddress;
|
||||
boost::asio::io_context::strand m_IoStrand;
|
||||
bool m_ShuttingDown;
|
||||
bool m_HasStartedStreaming;
|
||||
bool m_ConnectionReusable;
|
||||
boost::asio::deadline_timer m_CheckLivenessTimer;
|
||||
|
||||
HttpServerConnection(const WaitGroup::Ptr& waitGroup, const String& identity, bool authenticated,
|
||||
|
|
|
|||
|
|
@ -52,16 +52,15 @@ Value HttpUtility::GetLastParameter(const Dictionary::Ptr& params, const String&
|
|||
return arr->Get(arr->GetLength() - 1);
|
||||
}
|
||||
|
||||
void HttpUtility::SendJsonBody(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const Value& val)
|
||||
void HttpUtility::SendJsonBody(HttpResponse& response, const Dictionary::Ptr& params, const Value& val)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
response.set(http::field::content_type, "application/json");
|
||||
response.body() = JsonEncode(val, params && GetLastParameter(params, "pretty"));
|
||||
response.content_length(response.body().size());
|
||||
response.GetJsonEncoder(params && GetLastParameter(params, "pretty")).Encode(val);
|
||||
}
|
||||
|
||||
void HttpUtility::SendJsonError(boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
void HttpUtility::SendJsonError(HttpResponse& response,
|
||||
const Dictionary::Ptr& params, int code, const String& info, const String& diagnosticInformation)
|
||||
{
|
||||
Dictionary::Ptr result = new Dictionary({ { "error", code } });
|
||||
|
|
@ -74,6 +73,7 @@ void HttpUtility::SendJsonError(boost::beast::http::response<boost::beast::http:
|
|||
result->Set("diagnostic_information", diagnosticInformation);
|
||||
}
|
||||
|
||||
response.Clear();
|
||||
response.result(code);
|
||||
|
||||
HttpUtility::SendJsonBody(response, params, result);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
#include "remote/url.hpp"
|
||||
#include "base/dictionary.hpp"
|
||||
#include <boost/beast/http.hpp>
|
||||
#include "remote/httpmessage.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace icinga
|
||||
|
|
@ -23,9 +23,9 @@ public:
|
|||
static Dictionary::Ptr FetchRequestParameters(const Url::Ptr& url, const std::string& body);
|
||||
static Value GetLastParameter(const Dictionary::Ptr& params, const String& key);
|
||||
|
||||
static void SendJsonBody(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const Value& val);
|
||||
static void SendJsonError(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const int code,
|
||||
const String& verbose = String(), const String& diagnosticInformation = String());
|
||||
static void SendJsonBody(HttpResponse& response, const Dictionary::Ptr& params, const Value& val);
|
||||
static void SendJsonError(HttpResponse& response, const Dictionary::Ptr& params, const int code,
|
||||
const String& info = {}, const String& diagnosticInformation = {});
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,18 +10,17 @@ REGISTER_URLHANDLER("/", InfoHandler);
|
|||
|
||||
bool InfoHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() > 2)
|
||||
return false;
|
||||
|
||||
|
|
@ -77,23 +76,23 @@ bool InfoHandler::HandleRequest(
|
|||
} else {
|
||||
response.set(http::field::content_type, "text/html");
|
||||
|
||||
String body = "<html><head><title>Icinga 2</title></head><h1>Hello from Icinga 2 (Version: " + Application::GetAppVersion() + ")!</h1>";
|
||||
body += "<p>You are authenticated as <b>" + user->GetName() + "</b>. ";
|
||||
auto& body = response.body();
|
||||
body << "<html><head><title>Icinga 2</title></head><h1>Hello from Icinga 2 (Version: "
|
||||
<< Application::GetAppVersion() << ")!</h1>"
|
||||
<< "<p>You are authenticated as <b>" << user->GetName() << "</b>. ";
|
||||
|
||||
if (!permInfo.empty()) {
|
||||
body += "Your user has the following permissions:</p> <ul>";
|
||||
body << "Your user has the following permissions:</p> <ul>";
|
||||
|
||||
for (const String& perm : permInfo) {
|
||||
body += "<li>" + perm + "</li>";
|
||||
body << "<li>" << perm << "</li>";
|
||||
}
|
||||
|
||||
body += "</ul>";
|
||||
body << "</ul>";
|
||||
} else
|
||||
body += "Your user does not have any permissions.</p>";
|
||||
body << "Your user does not have any permissions.</p>";
|
||||
|
||||
body += R"(<p>More information about API requests is available in the <a href="https://icinga.com/docs/icinga2/latest/" target="_blank">documentation</a>.</p></html>)";
|
||||
response.body() = body;
|
||||
response.content_length(response.body().size());
|
||||
body << R"(<p>More information about API requests is available in the <a href="https://icinga.com/docs/icinga2/latest/" target="_blank">documentation</a>.</p></html>)";
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -19,18 +19,17 @@ REGISTER_URLHANDLER("/v1/debug/malloc_info", MallocInfoHandler);
|
|||
|
||||
bool MallocInfoHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream&,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context&,
|
||||
HttpServerConnection&
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context&
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() != 3) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -87,8 +86,7 @@ bool MallocInfoHandler::HandleRequest(
|
|||
|
||||
response.result(200);
|
||||
response.set(http::field::content_type, "application/xml");
|
||||
response.body() = std::string(buf, bufSize);
|
||||
response.content_length(response.body().size());
|
||||
response.body() << std::string_view(buf, bufSize);
|
||||
#endif /* HAVE_MALLOC_INFO */
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -14,14 +14,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,18 +15,17 @@ REGISTER_URLHANDLER("/v1/objects", ModifyObjectHandler);
|
|||
|
||||
bool ModifyObjectHandler::HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#include "remote/objectqueryhandler.hpp"
|
||||
#include "base/generator.hpp"
|
||||
#include "base/json.hpp"
|
||||
#include "remote/httputility.hpp"
|
||||
#include "remote/filterutility.hpp"
|
||||
#include "base/serializer.hpp"
|
||||
|
|
@ -9,6 +11,7 @@
|
|||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
|
||||
using namespace icinga;
|
||||
|
||||
|
|
@ -90,18 +93,17 @@ Dictionary::Ptr ObjectQueryHandler::SerializeObjectAttrs(const Object::Ptr& obje
|
|||
|
||||
bool ObjectQueryHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
|
||||
return false;
|
||||
|
||||
|
|
@ -145,6 +147,22 @@ bool ObjectQueryHandler::HandleRequest(
|
|||
return true;
|
||||
}
|
||||
|
||||
bool includeUsedBy = false;
|
||||
bool includeLocation = false;
|
||||
if (umetas) {
|
||||
ObjectLock olock(umetas);
|
||||
for (String meta : umetas) {
|
||||
if (meta == "used_by") {
|
||||
includeUsedBy = true;
|
||||
} else if (meta == "location") {
|
||||
includeLocation = true;
|
||||
} else {
|
||||
HttpUtility::SendJsonError(response, params, 400, "Invalid field specified for meta: " + meta);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool allJoins = HttpUtility::GetLastParameter(params, "all_joins");
|
||||
|
||||
params->Set("type", type->GetName());
|
||||
|
|
@ -166,10 +184,7 @@ bool ObjectQueryHandler::HandleRequest(
|
|||
return true;
|
||||
}
|
||||
|
||||
ArrayData results;
|
||||
results.reserve(objs.size());
|
||||
|
||||
std::set<String> joinAttrs;
|
||||
std::set<int> joinAttrs;
|
||||
std::set<String> userJoinAttrs;
|
||||
|
||||
if (ujoins) {
|
||||
|
|
@ -188,70 +203,63 @@ bool ObjectQueryHandler::HandleRequest(
|
|||
if (!allJoins && userJoinAttrs.find(field.NavigationName) == userJoinAttrs.end())
|
||||
continue;
|
||||
|
||||
joinAttrs.insert(field.Name);
|
||||
joinAttrs.insert(fid);
|
||||
}
|
||||
|
||||
std::unordered_map<Type*, std::pair<bool, std::unique_ptr<Expression>>> typePermissions;
|
||||
std::unordered_map<Object*, bool> objectAccessAllowed;
|
||||
|
||||
for (ConfigObject::Ptr obj : objs) {
|
||||
auto it = objs.begin();
|
||||
auto generatorFunc = [&]() -> std::optional<Value> {
|
||||
if (it == objs.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ConfigObject::Ptr obj = *it;
|
||||
++it;
|
||||
|
||||
DictionaryData result1{
|
||||
{ "name", obj->GetName() },
|
||||
{ "type", obj->GetReflectionType()->GetName() }
|
||||
};
|
||||
|
||||
DictionaryData metaAttrs;
|
||||
if (includeUsedBy) {
|
||||
Array::Ptr used_by = new Array();
|
||||
metaAttrs.emplace_back("used_by", used_by);
|
||||
|
||||
if (umetas) {
|
||||
ObjectLock olock(umetas);
|
||||
for (String meta : umetas) {
|
||||
if (meta == "used_by") {
|
||||
Array::Ptr used_by = new Array();
|
||||
metaAttrs.emplace_back("used_by", used_by);
|
||||
|
||||
for (auto& configObj : DependencyGraph::GetChildren(obj)) {
|
||||
used_by->Add(new Dictionary({
|
||||
{ "type", configObj->GetReflectionType()->GetName() },
|
||||
{ "name", configObj->GetName() }
|
||||
}));
|
||||
}
|
||||
} else if (meta == "location") {
|
||||
metaAttrs.emplace_back("location", obj->GetSourceLocation());
|
||||
} else {
|
||||
HttpUtility::SendJsonError(response, params, 400, "Invalid field specified for meta: " + meta);
|
||||
return true;
|
||||
}
|
||||
for (auto& configObj : DependencyGraph::GetChildren(obj)) {
|
||||
used_by->Add(new Dictionary({
|
||||
{"type", configObj->GetReflectionType()->GetName()},
|
||||
{"name", configObj->GetName()}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (includeLocation) {
|
||||
metaAttrs.emplace_back("location", obj->GetSourceLocation());
|
||||
}
|
||||
|
||||
result1.emplace_back("meta", new Dictionary(std::move(metaAttrs)));
|
||||
|
||||
try {
|
||||
result1.emplace_back("attrs", SerializeObjectAttrs(obj, String(), uattrs, false, false));
|
||||
} catch (const ScriptError& ex) {
|
||||
HttpUtility::SendJsonError(response, params, 400, ex.what());
|
||||
return true;
|
||||
return new Dictionary{
|
||||
{"type", type->GetName()},
|
||||
{"name", obj->GetName()},
|
||||
{"code", 400},
|
||||
{"status", ex.what()}
|
||||
};
|
||||
}
|
||||
|
||||
DictionaryData joins;
|
||||
|
||||
for (const String& joinAttr : joinAttrs) {
|
||||
for (auto joinAttr : joinAttrs) {
|
||||
Object::Ptr joinedObj;
|
||||
int fid = type->GetFieldId(joinAttr);
|
||||
Field field = type->GetFieldInfo(joinAttr);
|
||||
|
||||
if (fid < 0) {
|
||||
HttpUtility::SendJsonError(response, params, 400, "Invalid field specified for join: " + joinAttr);
|
||||
return true;
|
||||
}
|
||||
|
||||
Field field = type->GetFieldInfo(fid);
|
||||
|
||||
if (!(field.Attributes & FANavigation)) {
|
||||
HttpUtility::SendJsonError(response, params, 400, "Not a joinable field: " + joinAttr);
|
||||
return true;
|
||||
}
|
||||
|
||||
joinedObj = obj->NavigateField(fid);
|
||||
joinedObj = obj->NavigateField(joinAttr);
|
||||
|
||||
if (!joinedObj)
|
||||
continue;
|
||||
|
|
@ -304,22 +312,29 @@ bool ObjectQueryHandler::HandleRequest(
|
|||
try {
|
||||
joins.emplace_back(prefix, SerializeObjectAttrs(joinedObj, prefix, ujoins, true, allJoins));
|
||||
} catch (const ScriptError& ex) {
|
||||
HttpUtility::SendJsonError(response, params, 400, ex.what());
|
||||
return true;
|
||||
return new Dictionary{
|
||||
{"type", type->GetName()},
|
||||
{"name", obj->GetName()},
|
||||
{"code", 400},
|
||||
{"status", ex.what()}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
result1.emplace_back("joins", new Dictionary(std::move(joins)));
|
||||
|
||||
results.push_back(new Dictionary(std::move(result1)));
|
||||
}
|
||||
|
||||
Dictionary::Ptr result = new Dictionary({
|
||||
{ "results", new Array(std::move(results)) }
|
||||
});
|
||||
return new Dictionary{std::move(result1)};
|
||||
};
|
||||
|
||||
response.result(http::status::ok);
|
||||
HttpUtility::SendJsonBody(response, params, result);
|
||||
response.set(http::field::content_type, "application/json");
|
||||
response.StartStreaming();
|
||||
|
||||
Dictionary::Ptr results = new Dictionary{{"results", new ValueGenerator{generatorFunc}}};
|
||||
results->Freeze();
|
||||
|
||||
bool pretty = HttpUtility::GetLastParameter(params, "pretty");
|
||||
response.GetJsonEncoder(pretty).Encode(results, &yc);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -70,18 +70,17 @@ public:
|
|||
|
||||
bool StatusHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() > 3)
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -77,18 +77,17 @@ public:
|
|||
|
||||
bool TemplateQueryHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -48,18 +48,17 @@ public:
|
|||
|
||||
bool TypeQueryHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() > 3)
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -58,18 +58,17 @@ public:
|
|||
|
||||
bool VariableQueryHandler::HandleRequest(
|
||||
const WaitGroup::Ptr&,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
auto url = request.Url();
|
||||
auto user = request.User();
|
||||
auto params = request.Params();
|
||||
|
||||
if (url->GetPath().size() > 3)
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@ public:
|
|||
|
||||
bool HandleRequest(
|
||||
const WaitGroup::Ptr& waitGroup,
|
||||
AsioTlsStream& stream,
|
||||
const ApiUser::Ptr& user,
|
||||
boost::beast::http::request<boost::beast::http::string_body>& request,
|
||||
const Url::Ptr& url,
|
||||
boost::beast::http::response<boost::beast::http::string_body>& response,
|
||||
const Dictionary::Ptr& params,
|
||||
boost::asio::yield_context& yc,
|
||||
HttpServerConnection& server
|
||||
const HttpRequest& request,
|
||||
HttpResponse& response,
|
||||
boost::asio::yield_context& yc
|
||||
) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,10 @@ set(base_test_SOURCES
|
|||
icinga-notification.cpp
|
||||
icinga-perfdata.cpp
|
||||
methods-pluginnotificationtask.cpp
|
||||
remote-certificate-fixture.cpp
|
||||
remote-configpackageutility.cpp
|
||||
remote-httpserverconnection.cpp
|
||||
remote-httpmessage.cpp
|
||||
remote-url.cpp
|
||||
${base_OBJS}
|
||||
$<TARGET_OBJECTS:config>
|
||||
|
|
@ -271,6 +274,33 @@ add_boost_test(base
|
|||
icinga_perfdata/parse_edgecases
|
||||
icinga_perfdata/empty_warn_crit_min_max
|
||||
methods_pluginnotificationtask/truncate_long_output
|
||||
remote_certs_fixture/prepare_directory
|
||||
remote_certs_fixture/cleanup_certs
|
||||
remote_httpmessage/request_parse
|
||||
remote_httpmessage/request_params
|
||||
remote_httpmessage/response_clear
|
||||
remote_httpmessage/response_flush_nothrow
|
||||
remote_httpmessage/response_flush_throw
|
||||
remote_httpmessage/response_write_empty
|
||||
remote_httpmessage/response_write_fixed
|
||||
remote_httpmessage/response_write_chunked
|
||||
remote_httpmessage/response_sendjsonbody
|
||||
remote_httpmessage/response_sendjsonerror
|
||||
remote_httpmessage/response_sendfile
|
||||
remote_httpserverconnection/expect_100_continue
|
||||
remote_httpserverconnection/bad_request
|
||||
remote_httpserverconnection/error_access_control
|
||||
remote_httpserverconnection/error_accept_header
|
||||
remote_httpserverconnection/authenticate_cn
|
||||
remote_httpserverconnection/authenticate_passwd
|
||||
remote_httpserverconnection/authenticate_error_wronguser
|
||||
remote_httpserverconnection/authenticate_error_wrongpasswd
|
||||
remote_httpserverconnection/reuse_connection
|
||||
remote_httpserverconnection/wg_abort
|
||||
remote_httpserverconnection/client_shutdown
|
||||
remote_httpserverconnection/handler_throw_error
|
||||
remote_httpserverconnection/handler_throw_streaming
|
||||
remote_httpserverconnection/liveness_disconnect
|
||||
remote_configpackageutility/ValidateName
|
||||
remote_url/id_and_path
|
||||
remote_url/parameters
|
||||
|
|
@ -279,6 +309,46 @@ add_boost_test(base
|
|||
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_clear
|
||||
base-remote_httpmessage/response_flush_nothrow
|
||||
base-remote_httpmessage/response_flush_throw
|
||||
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_httpmessage/response_sendfile
|
||||
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/authenticate_cn
|
||||
base-remote_httpserverconnection/authenticate_passwd
|
||||
base-remote_httpserverconnection/authenticate_error_wronguser
|
||||
base-remote_httpserverconnection/authenticate_error_wrongpasswd
|
||||
base-remote_httpserverconnection/reuse_connection
|
||||
base-remote_httpserverconnection/wg_abort
|
||||
base-remote_httpserverconnection/client_shutdown
|
||||
base-remote_httpserverconnection/handler_throw_error
|
||||
base-remote_httpserverconnection/handler_throw_streaming
|
||||
base-remote_httpserverconnection/liveness_disconnect
|
||||
PROPERTIES FIXTURES_REQUIRED ssl_certs)
|
||||
|
||||
set_tests_properties(
|
||||
base-remote_certs_fixture/prepare_directory
|
||||
PROPERTIES FIXTURES_SETUP ssl_certs
|
||||
)
|
||||
|
||||
set_tests_properties(
|
||||
base-remote_certs_fixture/cleanup_certs
|
||||
PROPERTIES FIXTURES_CLEANUP ssl_certs
|
||||
)
|
||||
endif()
|
||||
|
||||
if(ICINGA2_WITH_LIVESTATUS)
|
||||
set(livestatus_test_SOURCES
|
||||
icingaapplication-fixture.cpp
|
||||
|
|
|
|||
56
test/base-configuration-fixture.hpp
Normal file
56
test/base-configuration-fixture.hpp
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* 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())
|
||||
{
|
||||
boost::filesystem::create_directories(m_DataDir);
|
||||
Configuration::DataDir = m_DataDir.string();
|
||||
}
|
||||
|
||||
~ConfigurationDataDirFixture()
|
||||
{
|
||||
boost::filesystem::remove_all(m_DataDir);
|
||||
Configuration::DataDir = m_PrevDataDir.string();
|
||||
}
|
||||
|
||||
boost::filesystem::path m_DataDir;
|
||||
|
||||
private:
|
||||
boost::filesystem::path m_PrevDataDir;
|
||||
};
|
||||
|
||||
struct ConfigurationCacheDirFixture
|
||||
{
|
||||
ConfigurationCacheDirFixture()
|
||||
: m_CacheDir(boost::filesystem::current_path() / "cache"), m_PrevCacheDir(Configuration::CacheDir.GetData())
|
||||
{
|
||||
boost::filesystem::create_directories(m_CacheDir);
|
||||
Configuration::CacheDir = m_CacheDir.string();
|
||||
}
|
||||
|
||||
~ConfigurationCacheDirFixture()
|
||||
{
|
||||
boost::filesystem::remove_all(m_CacheDir);
|
||||
Configuration::CacheDir = m_PrevCacheDir.string();
|
||||
}
|
||||
|
||||
boost::filesystem::path m_CacheDir;
|
||||
|
||||
private:
|
||||
boost::filesystem::path m_PrevCacheDir;
|
||||
};
|
||||
|
||||
} // namespace icinga
|
||||
|
||||
#endif // CONFIGURATION_FIXTURE_H
|
||||
127
test/base-testloggerfixture.hpp
Normal file
127
test/base-testloggerfixture.hpp
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#ifndef TEST_LOGGER_FIXTURE_H
|
||||
#define TEST_LOGGER_FIXTURE_H
|
||||
|
||||
#include <BoostTestTargetConfig.h>
|
||||
#include "base/logger.hpp"
|
||||
#include <boost/range/algorithm.hpp>
|
||||
#include <boost/regex.hpp>
|
||||
#include <boost/test/test_tools.hpp>
|
||||
#include <future>
|
||||
|
||||
namespace icinga {
|
||||
|
||||
class TestLogger : public Logger
|
||||
{
|
||||
public:
|
||||
DECLARE_PTR_TYPEDEFS(TestLogger);
|
||||
|
||||
struct Expect
|
||||
{
|
||||
std::string pattern;
|
||||
std::promise<bool> prom;
|
||||
};
|
||||
|
||||
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 expect = std::make_shared<Expect>(Expect{pattern, std::promise<bool>()});
|
||||
m_Expects.emplace_back(expect);
|
||||
lock.unlock();
|
||||
|
||||
auto future = expect->prom.get_future();
|
||||
auto status = future.wait_for(timeout);
|
||||
boost::test_tools::assertion_result ret{status == std::future_status::ready && future.get()};
|
||||
ret.message() << "Pattern \"" << pattern << "\" in log within " << timeout.count() << "ms";
|
||||
|
||||
lock.lock();
|
||||
m_Expects.erase(boost::range::remove(m_Expects, expect), m_Expects.end());
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private:
|
||||
void ProcessLogEntry(const LogEntry& entry) override
|
||||
{
|
||||
std::unique_lock lock(m_Mutex);
|
||||
m_LogEntries.push_back(entry);
|
||||
|
||||
auto it = boost::range::remove_if(m_Expects, [&entry](const std::shared_ptr<Expect>& expect) {
|
||||
if (boost::regex_match(entry.Message.GetData(), boost::regex(expect->pattern))) {
|
||||
expect->prom.set_value(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
m_Expects.erase(it, m_Expects.end());
|
||||
}
|
||||
|
||||
void Flush() override {}
|
||||
|
||||
std::mutex m_Mutex;
|
||||
std::vector<std::shared_ptr<Expect>> m_Expects;
|
||||
std::vector<LogEntry> m_LogEntries;
|
||||
};
|
||||
|
||||
/**
|
||||
* A fixture to capture log entries and assert their presence in tests.
|
||||
*
|
||||
* Currently, this only supports checking existing entries and waiting for new ones
|
||||
* using ExpectLogPattern(), but more functionality can easily be added in the future,
|
||||
* like only asserting on past log messages, only waiting for new ones, asserting log
|
||||
* entry metadata (severity etc.) and so on.
|
||||
*/
|
||||
struct TestLoggerFixture
|
||||
{
|
||||
TestLoggerFixture()
|
||||
{
|
||||
testLogger->SetSeverity(testLogger->SeverityToString(LogDebug));
|
||||
testLogger->Activate(true);
|
||||
testLogger->SetActive(true);
|
||||
}
|
||||
|
||||
~TestLoggerFixture()
|
||||
{
|
||||
testLogger->SetActive(false);
|
||||
testLogger->Deactivate(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the presence of a log entry that matches the given regex pattern.
|
||||
*
|
||||
* First, the existing log entries are searched for the pattern. If the pattern isn't found,
|
||||
* until the timeout is reached, the function will wait if a new log message is added that
|
||||
* matches the pattern.
|
||||
*
|
||||
* A boost assertion result object is returned, that evaluates to bool, but contains an
|
||||
* error message that is printed by the testing framework when the assert failed.
|
||||
*
|
||||
* @param pattern The regex pattern the log message needs to match
|
||||
* @param timeout The maximum amount of time to wait for the log message to arrive
|
||||
*
|
||||
* @return A @c boost::test_tools::assertion_result object that can be used in BOOST_REQUIRE
|
||||
*/
|
||||
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
|
||||
114
test/base-tlsstream-fixture.hpp
Normal file
114
test/base-tlsstream-fixture.hpp
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "base/io-engine.hpp"
|
||||
#include "base/tlsstream.hpp"
|
||||
#include "test/remote-certificate-fixture.hpp"
|
||||
#include <BoostTestTargetConfig.h>
|
||||
#include <future>
|
||||
|
||||
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& io = IoEngine::Get().GetIoContext();
|
||||
|
||||
m_ClientSslContext = SetupSslContext(clientCert.crtFile, clientCert.keyFile, m_CaCrtFile.string(), "",
|
||||
DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
|
||||
client = Shared<AsioTlsStream>::Make(io, *m_ClientSslContext);
|
||||
|
||||
m_ServerSslContext = SetupSslContext(serverCert.crtFile, serverCert.keyFile, m_CaCrtFile.string(), "",
|
||||
DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
|
||||
server = Shared<AsioTlsStream>::Make(io, *m_ServerSslContext);
|
||||
|
||||
std::promise<void> p;
|
||||
|
||||
tcp::acceptor acceptor{io, tcp::endpoint{address_v4::loopback(), 0}};
|
||||
acceptor.listen();
|
||||
acceptor.async_accept(server->lowest_layer(), [&](const boost::system::error_code& ec) {
|
||||
if (ec) {
|
||||
BOOST_TEST_MESSAGE("Server Accept Error: " + ec.message());
|
||||
p.set_exception(std::make_exception_ptr(boost::system::system_error{ec}));
|
||||
return;
|
||||
}
|
||||
server->next_layer().async_handshake(handshake_type::server, [&](const boost::system::error_code& ec) {
|
||||
if (ec) {
|
||||
BOOST_TEST_MESSAGE("Server Handshake Error: " + ec.message());
|
||||
p.set_exception(std::make_exception_ptr(boost::system::system_error{ec}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!server->next_layer().IsVerifyOK()) {
|
||||
p.set_exception(std::make_exception_ptr(std::runtime_error{"Verify failed on server-side."}));
|
||||
}
|
||||
|
||||
p.set_value();
|
||||
});
|
||||
});
|
||||
|
||||
auto f = p.get_future();
|
||||
boost::system::error_code ec;
|
||||
if (client->lowest_layer().connect(acceptor.local_endpoint(), ec)) {
|
||||
BOOST_TEST_MESSAGE("Client Connect error: " + ec.message());
|
||||
f.get();
|
||||
BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
|
||||
}
|
||||
|
||||
if (client->next_layer().handshake(handshake_type::client, ec)) {
|
||||
BOOST_TEST_MESSAGE("Client Handshake error: " + ec.message());
|
||||
f.get();
|
||||
BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
|
||||
}
|
||||
|
||||
if (!client->next_layer().IsVerifyOK()) {
|
||||
f.get();
|
||||
BOOST_THROW_EXCEPTION(std::runtime_error{"Verify failed on client-side."});
|
||||
}
|
||||
|
||||
f.get();
|
||||
}
|
||||
|
||||
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<AsioTlsStream>::Ptr client;
|
||||
Shared<AsioTlsStream>::Ptr server;
|
||||
|
||||
private:
|
||||
Shared<boost::asio::ssl::context>::Ptr m_ClientSslContext;
|
||||
Shared<boost::asio::ssl::context>::Ptr m_ServerSslContext;
|
||||
};
|
||||
|
||||
} // namespace icinga
|
||||
42
test/remote-certificate-fixture.cpp
Normal file
42
test/remote-certificate-fixture.cpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#include "remote-certificate-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_fixture)
|
||||
|
||||
/**
|
||||
* Recursively removes the directory that contains the test certificates.
|
||||
*
|
||||
* This needs to be done once initially to prepare the directory, in case there are any
|
||||
* left-overs from previous test runs, and once after all tests using the certificates
|
||||
* have been completed.
|
||||
*
|
||||
* This dependency is expressed as a CTest fixture and not a boost-test one, because that
|
||||
* is the only way to have persistency between individual test-cases with CTest.
|
||||
*/
|
||||
static void CleanupPersistentCertificateDir()
|
||||
{
|
||||
if (boost::filesystem::exists(CertificateFixture::m_PersistentCertsDir)) {
|
||||
boost::filesystem::remove_all(CertificateFixture::m_PersistentCertsDir);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(prepare_directory, ConfigurationDataDirFixture)
|
||||
{
|
||||
// Remove any existing left-overs of the persistent certificate directory from a previous
|
||||
// test run.
|
||||
CleanupPersistentCertificateDir();
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(cleanup_certs, ConfigurationDataDirFixture)
|
||||
{
|
||||
CleanupPersistentCertificateDir();
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
69
test/remote-certificate-fixture.hpp
Normal file
69
test/remote-certificate-fixture.hpp
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#pragma once
|
||||
|
||||
#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 Cert
|
||||
{
|
||||
String crtFile;
|
||||
String keyFile;
|
||||
String csrFile;
|
||||
};
|
||||
|
||||
Cert 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
|
||||
351
test/remote-httpmessage.cpp
Normal file
351
test/remote-httpmessage.cpp
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#include <BoostTestTargetConfig.h>
|
||||
#include "base/base64.hpp"
|
||||
#include "base/json.hpp"
|
||||
#include "remote/httpmessage.hpp"
|
||||
#include "remote/httputility.hpp"
|
||||
#include "test/base-tlsstream-fixture.hpp"
|
||||
#include <fstream>
|
||||
#include <utility>
|
||||
|
||||
using namespace icinga;
|
||||
using namespace boost::beast;
|
||||
|
||||
static std::future<void> SpawnSynchronizedCoroutine(std::function<void(boost::asio::yield_context)> fn)
|
||||
{
|
||||
auto promise = std::make_unique<std::promise<void>>();
|
||||
auto future = promise->get_future();
|
||||
auto& io = IoEngine::Get().GetIoContext();
|
||||
IoEngine::SpawnCoroutine(io, [promise = std::move(promise), fn = std::move(fn)](boost::asio::yield_context yc) {
|
||||
try {
|
||||
fn(std::move(yc));
|
||||
} catch (const std::exception&) {
|
||||
promise->set_exception(std::current_exception());
|
||||
return;
|
||||
}
|
||||
promise->set_value();
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
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.body() = "test";
|
||||
requestOut.prepare_payload();
|
||||
|
||||
auto future = SpawnSynchronizedCoroutine([this, &requestOut](boost::asio::yield_context yc) {
|
||||
boost::beast::flat_buffer buf;
|
||||
HttpRequest request(server);
|
||||
BOOST_REQUIRE_NO_THROW(request.ParseHeader(buf, yc));
|
||||
|
||||
for (const auto& field : requestOut.base()) {
|
||||
BOOST_REQUIRE(request.count(field.name()));
|
||||
}
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(request.ParseBody(buf, yc));
|
||||
BOOST_REQUIRE_EQUAL(request.body(), "test");
|
||||
|
||||
Shutdown(server, yc);
|
||||
});
|
||||
|
||||
http::write(*client, requestOut);
|
||||
client->flush();
|
||||
|
||||
Shutdown(client);
|
||||
future.get();
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(request_params)
|
||||
{
|
||||
HttpRequest request(client);
|
||||
// clang-format off
|
||||
request.body() = JsonEncode(
|
||||
new Dictionary{
|
||||
{"bool-in-json", true},
|
||||
{"bool-in-url-and-json", true},
|
||||
{"string-in-json", "json-value"},
|
||||
{"string-in-url-and-json", "json-value"}
|
||||
});
|
||||
request.target("https://localhost:1234/v1/test?"
|
||||
"bool-in-url-and-json=0&"
|
||||
"bool-in-url=1&"
|
||||
"string-in-url-and-json=url-value&"
|
||||
"string-only-in-url=url-value"
|
||||
);
|
||||
// clang-format on
|
||||
|
||||
// Test pointer being valid after decode
|
||||
request.DecodeParams();
|
||||
auto params = request.Params();
|
||||
BOOST_REQUIRE(params);
|
||||
|
||||
// Test JSON-only params being parsed as their correct type
|
||||
BOOST_REQUIRE(params->Get("bool-in-json").IsBoolean());
|
||||
BOOST_REQUIRE(params->Get("string-in-json").IsString());
|
||||
BOOST_REQUIRE(params->Get("bool-in-url-and-json").IsObjectType<Array>());
|
||||
BOOST_REQUIRE(params->Get("string-in-url-and-json").IsObjectType<Array>());
|
||||
|
||||
// Test 0/1 string values from URL evaluate to true and false
|
||||
// These currently get implicitly converted to double and then to bool, but this is an
|
||||
// implementation we don't need to test for here.
|
||||
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "bool-in-url-and-json"), "0");
|
||||
BOOST_REQUIRE(!HttpUtility::GetLastParameter(params, "bool-in-url-and-json"));
|
||||
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "bool-in-url"), "1");
|
||||
BOOST_REQUIRE(HttpUtility::GetLastParameter(params, "bool-in-url"));
|
||||
|
||||
// Test non-existing parameters evaluate to false
|
||||
BOOST_REQUIRE(HttpUtility::GetLastParameter(params, "does-not-exist").IsEmpty());
|
||||
BOOST_REQUIRE(!HttpUtility::GetLastParameter(params, "does-not-exist"));
|
||||
|
||||
// Test precedence of URL params over JSON params
|
||||
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "string-in-json"), "json-value");
|
||||
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "string-in-url-and-json"), "url-value");
|
||||
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "string-only-in-url"), "url-value");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_clear)
|
||||
{
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::bad_request);
|
||||
response.version(10);
|
||||
response.set(http::field::content_type, "text/html");
|
||||
response.body() << "test";
|
||||
|
||||
response.Clear();
|
||||
|
||||
BOOST_REQUIRE(response[http::field::content_type].empty());
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.body().Size(), 0);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_flush_nothrow)
|
||||
{
|
||||
auto future = SpawnSynchronizedCoroutine([this](const 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);
|
||||
});
|
||||
|
||||
auto status = future.wait_for(std::chrono::seconds(1));
|
||||
BOOST_REQUIRE(status == std::future_status::ready);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_flush_throw)
|
||||
{
|
||||
auto future = SpawnSynchronizedCoroutine([this](const boost::asio::yield_context& yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
|
||||
server->lowest_layer().close();
|
||||
|
||||
BOOST_REQUIRE_EXCEPTION(response.Flush(yc), std::exception, [](const std::exception& ex) {
|
||||
auto se = dynamic_cast<const boost::system::system_error*>(&ex);
|
||||
return se && se->code() == boost::system::errc::bad_file_descriptor;
|
||||
});
|
||||
});
|
||||
|
||||
auto status = future.wait_for(std::chrono::seconds(1));
|
||||
BOOST_REQUIRE(status == std::future_status::ready);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_write_empty)
|
||||
{
|
||||
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
Shutdown(server, yc);
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
boost::system::error_code ec;
|
||||
http::read(*client, buf, parser, ec);
|
||||
|
||||
Shutdown(client);
|
||||
|
||||
future.get();
|
||||
|
||||
BOOST_REQUIRE(!ec);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_write_fixed)
|
||||
{
|
||||
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
response.body() << "test";
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
Shutdown(server, yc);
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
boost::system::error_code ec;
|
||||
http::read(*client, buf, parser, ec);
|
||||
|
||||
Shutdown(client);
|
||||
|
||||
future.get();
|
||||
|
||||
BOOST_REQUIRE(!ec);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "test");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_write_chunked)
|
||||
{
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
|
||||
response.StartStreaming();
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
BOOST_REQUIRE(response.HasSerializationStarted());
|
||||
|
||||
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));
|
||||
|
||||
Shutdown(server, yc);
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
boost::system::error_code ec;
|
||||
http::read(*client, buf, parser, ec);
|
||||
|
||||
Shutdown(client);
|
||||
|
||||
future.get();
|
||||
|
||||
BOOST_REQUIRE(!ec);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), true);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "test1test2test3");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_sendjsonbody)
|
||||
{
|
||||
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
|
||||
HttpUtility::SendJsonBody(response, nullptr, new Dictionary{{"test", 1}});
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
Shutdown(server, yc);
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
boost::system::error_code ec;
|
||||
http::read(*client, buf, parser, ec);
|
||||
|
||||
Shutdown(client);
|
||||
|
||||
future.get();
|
||||
|
||||
BOOST_REQUIRE(!ec);
|
||||
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_AUTO_TEST_CASE(response_sendjsonerror)
|
||||
{
|
||||
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
|
||||
// This has to be overwritten in SendJsonError.
|
||||
response.result(http::status::ok);
|
||||
|
||||
HttpUtility::SendJsonError(response, nullptr, 404, "Not found.");
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
Shutdown(server, yc);
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
boost::system::error_code ec;
|
||||
http::read(*client, buf, parser, ec);
|
||||
|
||||
Shutdown(client);
|
||||
|
||||
future.get();
|
||||
|
||||
BOOST_REQUIRE(!ec);
|
||||
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_AUTO_TEST_CASE(response_sendfile)
|
||||
{
|
||||
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
|
||||
response.result(http::status::ok);
|
||||
BOOST_REQUIRE_NO_THROW(response.SendFile(m_CaCrtFile.string(), yc));
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
Shutdown(server, yc);
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
boost::system::error_code ec;
|
||||
http::read(*client, buf, parser, ec);
|
||||
|
||||
Shutdown(client);
|
||||
|
||||
future.get();
|
||||
|
||||
BOOST_REQUIRE(!ec);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||
|
||||
std::ifstream fp(m_CaCrtFile.string(), std::ifstream::in | std::ifstream::binary);
|
||||
fp.exceptions(std::ifstream::badbit);
|
||||
std::stringstream ss;
|
||||
ss << fp.rdbuf();
|
||||
BOOST_REQUIRE_EQUAL(ss.str(), parser.get().body());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
558
test/remote-httpserverconnection.cpp
Normal file
558
test/remote-httpserverconnection.cpp
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#include <BoostTestTargetConfig.h>
|
||||
#include "base/base64.hpp"
|
||||
#include "base/json.hpp"
|
||||
#include "remote/httphandler.hpp"
|
||||
#include "test/base-testloggerfixture.hpp"
|
||||
#include "test/base-tlsstream-fixture.hpp"
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <utility>
|
||||
|
||||
using namespace icinga;
|
||||
using namespace boost::beast;
|
||||
using namespace boost::unit_test_framework;
|
||||
|
||||
struct HttpServerConnectionFixture : TlsStreamFixture, ConfigurationCacheDirFixture, TestLoggerFixture
|
||||
{
|
||||
HttpServerConnection::Ptr m_Connection;
|
||||
StoppableWaitGroup::Ptr m_WaitGroup;
|
||||
|
||||
HttpServerConnectionFixture() : m_WaitGroup(new StoppableWaitGroup) {}
|
||||
|
||||
static void CreateApiListener(const String& allowOrigin)
|
||||
{
|
||||
ScriptGlobal::Set("NodeName", "server");
|
||||
ApiListener::Ptr listener = new ApiListener;
|
||||
listener->OnConfigLoaded();
|
||||
listener->SetAccessControlAllowOrigin(new Array{allowOrigin});
|
||||
}
|
||||
|
||||
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";
|
||||
m_Connection = new HttpServerConnection(m_WaitGroup, identity, authenticated, server);
|
||||
m_Connection->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 && !m_Connection->Disconnected(); i++) {
|
||||
Utility::Sleep(std::chrono::duration<double>(timeout).count() / iterations);
|
||||
}
|
||||
return m_Connection->Disconnected();
|
||||
}
|
||||
};
|
||||
|
||||
class UnitTestHandler final : public HttpHandler
|
||||
{
|
||||
public:
|
||||
using TestFn = std::function<void(HttpResponse& response, const boost::asio::yield_context&)>;
|
||||
|
||||
static void RegisterTestFn(std::string handle, TestFn fn) { testFns[std::move(handle)] = std::move(fn); }
|
||||
|
||||
private:
|
||||
bool HandleRequest(const WaitGroup::Ptr&, const HttpRequest& request, HttpResponse& response,
|
||||
boost::asio::yield_context& yc) override
|
||||
{
|
||||
response.result(boost::beast::http::status::ok);
|
||||
|
||||
auto path = request.Url()->GetPath();
|
||||
|
||||
if (path.size() == 3) {
|
||||
if (auto it = testFns.find(path[2].GetData()); it != testFns.end()) {
|
||||
it->second(response, yc);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
response.body() << "test";
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline std::unordered_map<std::string, TestFn> testFns;
|
||||
};
|
||||
|
||||
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("/v1/test");
|
||||
request.set(http::field::expect, "100-continue");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
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;
|
||||
BOOST_REQUIRE_NO_THROW(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();
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(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("/v1/test");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
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;
|
||||
BOOST_REQUIRE_NO_THROW(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("example.org");
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::options);
|
||||
request.target("/v1/test");
|
||||
request.set(http::field::origin, "example.org");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
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;
|
||||
BOOST_REQUIRE_NO_THROW(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_REQUIRE_EQUAL(response[http::field::access_control_allow_credentials], "true");
|
||||
BOOST_REQUIRE_EQUAL(response[http::field::access_control_allow_origin], "example.org");
|
||||
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("/v1/test");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
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;
|
||||
BOOST_REQUIRE_NO_THROW(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>");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(authenticate_cn)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("/v1/test");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
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;
|
||||
BOOST_REQUIRE_NO_THROW(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(authenticate_passwd)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(false);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("/v1/test");
|
||||
request.set(http::field::authorization, "Basic " + Base64::Encode("test:test"));
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
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;
|
||||
BOOST_REQUIRE_NO_THROW(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(authenticate_error_wronguser)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(false);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("/v1/test");
|
||||
request.set(http::field::authorization, "Basic " + Base64::Encode("invalid:invalid"));
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
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;
|
||||
BOOST_REQUIRE_NO_THROW(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(authenticate_error_wrongpasswd)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(false);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("/v1/test");
|
||||
request.set(http::field::authorization, "Basic " + Base64::Encode("test:invalid"));
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
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;
|
||||
BOOST_REQUIRE_NO_THROW(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("/v1/test");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
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;
|
||||
BOOST_REQUIRE_NO_THROW(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();
|
||||
|
||||
boost::system::error_code ec;
|
||||
http::response_parser<http::string_body> parser;
|
||||
BOOST_REQUIRE_NO_THROW(http::read(*client, buf, parser));
|
||||
|
||||
BOOST_REQUIRE(parser.is_header_done());
|
||||
BOOST_REQUIRE(parser.is_done());
|
||||
BOOST_REQUIRE_EQUAL(parser.get().version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "test");
|
||||
|
||||
// Second read to get the end of stream error;
|
||||
http::read(*client, buf, response, ec);
|
||||
BOOST_REQUIRE_EQUAL(ec, boost::system::error_code{boost::beast::http::error::end_of_stream});
|
||||
|
||||
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);
|
||||
|
||||
UnitTestHandler::RegisterTestFn("wgjoin", [this](HttpResponse& response, const boost::asio::yield_context&) {
|
||||
response.body() << "test";
|
||||
m_WaitGroup->Join();
|
||||
});
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("/v1/test/wgjoin");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.keep_alive(true);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response_parser<http::string_body> parser;
|
||||
BOOST_REQUIRE_NO_THROW(http::read(*client, buf, parser));
|
||||
|
||||
BOOST_REQUIRE(parser.is_header_done());
|
||||
BOOST_REQUIRE(parser.is_done());
|
||||
BOOST_REQUIRE_EQUAL(parser.get().version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "test");
|
||||
|
||||
// Second read to get the end of stream error;
|
||||
http::response<http::string_body> response{};
|
||||
boost::system::error_code ec;
|
||||
http::read(*client, buf, response, ec);
|
||||
BOOST_REQUIRE_EQUAL(ec, boost::system::error_code{boost::beast::http::error::end_of_stream});
|
||||
|
||||
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);
|
||||
|
||||
UnitTestHandler::RegisterTestFn("stream", [](HttpResponse& response, const boost::asio::yield_context& yc) {
|
||||
response.StartStreaming();
|
||||
response.Flush(yc);
|
||||
|
||||
boost::asio::deadline_timer dt{IoEngine::Get().GetIoContext()};
|
||||
for (;;) {
|
||||
dt.expires_from_now(boost::posix_time::seconds(1));
|
||||
dt.async_wait(yc);
|
||||
|
||||
if (!response.IsClientDisconnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
response.body() << "test";
|
||||
response.Flush(yc);
|
||||
}
|
||||
});
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("/v1/test/stream");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.keep_alive(true);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response_parser<http::string_body> parser;
|
||||
BOOST_REQUIRE_NO_THROW(http::read_header(*client, buf, parser));
|
||||
BOOST_REQUIRE(parser.is_header_done());
|
||||
|
||||
/* 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 may 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_AUTO_TEST_CASE(handler_throw_error)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
UnitTestHandler::RegisterTestFn("throw", [](HttpResponse& response, const boost::asio::yield_context&) {
|
||||
response.StartStreaming();
|
||||
response.body() << "test";
|
||||
|
||||
boost::system::error_code ec{};
|
||||
throw boost::system::system_error(ec);
|
||||
});
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("/v1/test/throw");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.keep_alive(false);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
BOOST_REQUIRE_NO_THROW(http::read(*client, buf, response));
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::internal_server_error);
|
||||
Dictionary::Ptr body = JsonDecode(response.body());
|
||||
BOOST_REQUIRE(body);
|
||||
BOOST_REQUIRE_EQUAL(body->Get("error"), 500);
|
||||
BOOST_REQUIRE_EQUAL(body->Get("status"), "Unhandled exception");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected .*", std::chrono::seconds(5)));
|
||||
BOOST_REQUIRE(!ExpectLogPattern("Exception while processing HTTP request.*"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(handler_throw_streaming)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
UnitTestHandler::RegisterTestFn("throw", [](HttpResponse& response, const boost::asio::yield_context& yc) {
|
||||
response.StartStreaming();
|
||||
response.body() << "test";
|
||||
|
||||
response.Flush(yc);
|
||||
|
||||
boost::system::error_code ec{};
|
||||
throw boost::system::system_error(ec);
|
||||
});
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("/v1/test/throw");
|
||||
request.set(http::field::host, "localhost:5665");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.keep_alive(true);
|
||||
|
||||
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.
|
||||
*/
|
||||
BOOST_REQUIRE_EQUAL(ec, boost::system::error_code{boost::beast::http::error::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.*"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(liveness_disconnect)
|
||||
{
|
||||
SetupHttpServerConnection(false);
|
||||
|
||||
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(11)));
|
||||
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected .*"));
|
||||
BOOST_REQUIRE(ExpectLogPattern("No messages for HTTP connection have been received in the last 10 seconds."));
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
Loading…
Reference in a new issue