mirror of
https://github.com/Icinga/icinga2.git
synced 2025-08-23 10:38:16 +02:00
When a HTTP connection dies prematurely while the response is sent, `http::async_write()` sets the error code to something like broken pipe for example. When calling `async_flush()` afterwards, it sometimes happens that this never returns. This results in a resource leak as the coroutine isn't cleaned up. This commit makes the individual functions throw exceptions instead of silently ignoring the errors, resulting in the function terminating early and also resulting in an error being logged as well.
592 lines
15 KiB
C++
592 lines
15 KiB
C++
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
|
|
|
|
#include "remote/httpserverconnection.hpp"
|
|
#include "remote/httphandler.hpp"
|
|
#include "remote/httputility.hpp"
|
|
#include "remote/apilistener.hpp"
|
|
#include "remote/apifunction.hpp"
|
|
#include "remote/jsonrpc.hpp"
|
|
#include "base/application.hpp"
|
|
#include "base/base64.hpp"
|
|
#include "base/convert.hpp"
|
|
#include "base/configtype.hpp"
|
|
#include "base/defer.hpp"
|
|
#include "base/exception.hpp"
|
|
#include "base/io-engine.hpp"
|
|
#include "base/logger.hpp"
|
|
#include "base/objectlock.hpp"
|
|
#include "base/timer.hpp"
|
|
#include "base/tlsstream.hpp"
|
|
#include "base/utility.hpp"
|
|
#include <chrono>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <boost/asio/error.hpp>
|
|
#include <boost/asio/io_context.hpp>
|
|
#include <boost/asio/spawn.hpp>
|
|
#include <boost/beast/core.hpp>
|
|
#include <boost/beast/http.hpp>
|
|
#include <boost/system/error_code.hpp>
|
|
#include <boost/system/system_error.hpp>
|
|
#include <boost/thread/once.hpp>
|
|
|
|
using namespace icinga;
|
|
|
|
auto const l_ServerHeader ("Icinga/" + Application::GetAppVersion());
|
|
|
|
HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream)
|
|
: HttpServerConnection(identity, authenticated, stream, IoEngine::Get().GetIoContext())
|
|
{
|
|
}
|
|
|
|
HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, boost::asio::io_context& io)
|
|
: m_Stream(stream), m_Seen(Utility::GetTime()), m_IoStrand(io), m_ShuttingDown(false), m_HasStartedStreaming(false),
|
|
m_CheckLivenessTimer(io)
|
|
{
|
|
if (authenticated) {
|
|
m_ApiUser = ApiUser::GetByClientCN(identity);
|
|
}
|
|
|
|
{
|
|
std::ostringstream address;
|
|
auto endpoint (stream->lowest_layer().remote_endpoint());
|
|
|
|
address << '[' << endpoint.address() << "]:" << endpoint.port();
|
|
|
|
m_PeerAddress = address.str();
|
|
}
|
|
}
|
|
|
|
void HttpServerConnection::Start()
|
|
{
|
|
namespace asio = boost::asio;
|
|
|
|
HttpServerConnection::Ptr keepAlive (this);
|
|
|
|
IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { ProcessMessages(yc); });
|
|
IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { CheckLiveness(yc); });
|
|
}
|
|
|
|
void HttpServerConnection::Disconnect()
|
|
{
|
|
namespace asio = boost::asio;
|
|
|
|
HttpServerConnection::Ptr keepAlive (this);
|
|
|
|
IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
|
|
if (!m_ShuttingDown) {
|
|
m_ShuttingDown = true;
|
|
|
|
Log(LogInformation, "HttpServerConnection")
|
|
<< "HTTP client disconnected (from " << m_PeerAddress << ")";
|
|
|
|
/*
|
|
* Do not swallow exceptions in a coroutine.
|
|
* https://github.com/Icinga/icinga2/issues/7351
|
|
* We must not catch `detail::forced_unwind exception` as
|
|
* this is used for unwinding the stack.
|
|
*
|
|
* Just use the error_code dummy here.
|
|
*/
|
|
boost::system::error_code ec;
|
|
|
|
m_CheckLivenessTimer.cancel();
|
|
|
|
m_Stream->lowest_layer().cancel(ec);
|
|
|
|
m_Stream->next_layer().async_shutdown(yc[ec]);
|
|
|
|
m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both, ec);
|
|
|
|
auto listener (ApiListener::GetInstance());
|
|
|
|
if (listener) {
|
|
listener->RemoveHttpClient(this);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void HttpServerConnection::StartStreaming()
|
|
{
|
|
namespace asio = boost::asio;
|
|
|
|
m_HasStartedStreaming = true;
|
|
|
|
HttpServerConnection::Ptr keepAlive (this);
|
|
|
|
IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
|
|
if (!m_ShuttingDown) {
|
|
char buf[128];
|
|
asio::mutable_buffer readBuf (buf, 128);
|
|
boost::system::error_code ec;
|
|
|
|
do {
|
|
m_Stream->async_read_some(readBuf, yc[ec]);
|
|
} while (!ec);
|
|
|
|
Disconnect();
|
|
}
|
|
});
|
|
}
|
|
|
|
bool HttpServerConnection::Disconnected()
|
|
{
|
|
return m_ShuttingDown;
|
|
}
|
|
|
|
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,
|
|
bool& shuttingDown,
|
|
boost::asio::yield_context& yc
|
|
)
|
|
{
|
|
namespace http = boost::beast::http;
|
|
|
|
if (shuttingDown)
|
|
return false;
|
|
|
|
bool httpError = false;
|
|
String errorMsg;
|
|
|
|
boost::system::error_code ec;
|
|
|
|
http::async_read_header(stream, buf, parser, yc[ec]);
|
|
|
|
if (ec) {
|
|
if (ec == boost::asio::error::operation_aborted)
|
|
return false;
|
|
|
|
errorMsg = ec.message();
|
|
httpError = true;
|
|
} else {
|
|
switch (parser.get().version()) {
|
|
case 10:
|
|
case 11:
|
|
break;
|
|
default:
|
|
errorMsg = "Unsupported HTTP version";
|
|
}
|
|
}
|
|
|
|
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 }
|
|
}));
|
|
} 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.set(http::field::connection, "close");
|
|
|
|
http::async_write(stream, response, yc);
|
|
stream.async_flush(yc);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static inline
|
|
void HandleExpect100(
|
|
AsioTlsStream& stream,
|
|
boost::beast::http::request<boost::beast::http::string_body>& request,
|
|
boost::asio::yield_context& yc
|
|
)
|
|
{
|
|
namespace http = boost::beast::http;
|
|
|
|
if (request[http::field::expect] == "100-continue") {
|
|
http::response<http::string_body> response;
|
|
|
|
response.result(http::status::continue_);
|
|
|
|
http::async_write(stream, response, yc);
|
|
stream.async_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,
|
|
boost::asio::yield_context& yc
|
|
)
|
|
{
|
|
namespace http = boost::beast::http;
|
|
|
|
auto listener (ApiListener::GetInstance());
|
|
|
|
if (listener) {
|
|
auto headerAllowOrigin (listener->GetAccessControlAllowOrigin());
|
|
|
|
if (headerAllowOrigin) {
|
|
auto allowedOrigins (headerAllowOrigin->ToSet<String>());
|
|
|
|
if (!allowedOrigins.empty()) {
|
|
auto& origin (request[http::field::origin]);
|
|
|
|
if (allowedOrigins.find(std::string(origin)) != allowedOrigins.end()) {
|
|
response.set(http::field::access_control_allow_origin, origin);
|
|
}
|
|
|
|
response.set(http::field::access_control_allow_credentials, "true");
|
|
|
|
if (request.method() == http::verb::options && !request[http::field::access_control_request_method].empty()) {
|
|
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.set(http::field::connection, "close");
|
|
|
|
http::async_write(stream, response, yc);
|
|
stream.async_flush(yc);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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,
|
|
boost::asio::yield_context& yc
|
|
)
|
|
{
|
|
namespace http = boost::beast::http;
|
|
|
|
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.set(http::field::connection, "close");
|
|
|
|
http::async_write(stream, response, yc);
|
|
stream.async_flush(yc);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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,
|
|
boost::asio::yield_context& yc
|
|
)
|
|
{
|
|
namespace http = boost::beast::http;
|
|
|
|
if (!authenticatedUser) {
|
|
Log(LogWarning, "HttpServerConnection")
|
|
<< "Unauthorized request: " << request.method_string() << ' ' << request.target();
|
|
|
|
response.result(http::status::unauthorized);
|
|
response.set(http::field::www_authenticate, "Basic realm=\"Icinga 2\"");
|
|
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." }
|
|
}));
|
|
} 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());
|
|
}
|
|
|
|
http::async_write(stream, response, yc);
|
|
stream.async_flush(yc);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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,
|
|
bool& shuttingDown,
|
|
boost::asio::yield_context& yc
|
|
)
|
|
{
|
|
namespace http = boost::beast::http;
|
|
|
|
{
|
|
size_t maxSize = 1024 * 1024;
|
|
Array::Ptr permissions = authenticatedUser->GetPermissions();
|
|
|
|
if (permissions) {
|
|
CpuBoundWork evalPermissions (yc);
|
|
|
|
ObjectLock olock(permissions);
|
|
|
|
for (const Value& permissionInfo : permissions) {
|
|
String permission;
|
|
|
|
if (permissionInfo.IsObjectType<Dictionary>()) {
|
|
permission = static_cast<Dictionary::Ptr>(permissionInfo)->Get("permission");
|
|
} else {
|
|
permission = permissionInfo;
|
|
}
|
|
|
|
static std::vector<std::pair<String, size_t>> specialContentLengthLimits {
|
|
{ "config/modify", 512 * 1024 * 1024 }
|
|
};
|
|
|
|
for (const auto& limitInfo : specialContentLengthLimits) {
|
|
if (limitInfo.second <= maxSize) {
|
|
continue;
|
|
}
|
|
|
|
if (Utility::Match(permission, limitInfo.first)) {
|
|
maxSize = limitInfo.second;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
parser.body_limit(maxSize);
|
|
}
|
|
|
|
if (shuttingDown)
|
|
return false;
|
|
|
|
boost::system::error_code ec;
|
|
|
|
http::async_read(stream, buf, parser, yc[ec]);
|
|
|
|
if (ec) {
|
|
if (ec == boost::asio::error::operation_aborted)
|
|
return false;
|
|
|
|
/**
|
|
* Unfortunately there's no way to tell an HTTP protocol error
|
|
* from an error on a lower layer:
|
|
*
|
|
* <https://github.com/boostorg/beast/issues/643>
|
|
*/
|
|
|
|
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() }
|
|
}));
|
|
} 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.set(http::field::connection, "close");
|
|
|
|
http::async_write(stream, response, yc);
|
|
stream.async_flush(yc);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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,
|
|
boost::asio::yield_context& yc
|
|
)
|
|
{
|
|
namespace http = boost::beast::http;
|
|
|
|
try {
|
|
CpuBoundWork handlingRequest (yc);
|
|
|
|
HttpHandler::ProcessRequest(stream, authenticatedUser, request, response, yc, server);
|
|
} 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) {
|
|
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;
|
|
}
|
|
|
|
if (hasStartedStreaming) {
|
|
return false;
|
|
}
|
|
|
|
http::async_write(stream, response, yc);
|
|
stream.async_flush(yc);
|
|
|
|
return true;
|
|
}
|
|
|
|
void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
|
|
{
|
|
namespace beast = boost::beast;
|
|
namespace http = beast::http;
|
|
namespace ch = std::chrono;
|
|
|
|
try {
|
|
/* Do not reset the buffer in the state machine.
|
|
* EnsureValidHeaders already reads from the stream into the buffer,
|
|
* EnsureValidBody continues. ProcessRequest() actually handles the request
|
|
* and needs the full buffer.
|
|
*/
|
|
beast::flat_buffer buf;
|
|
|
|
for (;;) {
|
|
m_Seen = Utility::GetTime();
|
|
|
|
http::parser<true, http::string_body> parser;
|
|
http::response<http::string_body> response;
|
|
|
|
parser.header_limit(1024 * 1024);
|
|
parser.body_limit(-1);
|
|
|
|
response.set(http::field::server, l_ServerHeader);
|
|
|
|
if (!EnsureValidHeaders(*m_Stream, buf, parser, 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"]));
|
|
|
|
if (method != http::verb::unknown) {
|
|
request.method(method);
|
|
}
|
|
}
|
|
|
|
HandleExpect100(*m_Stream, request, yc);
|
|
|
|
auto authenticatedUser (m_ApiUser);
|
|
|
|
if (!authenticatedUser) {
|
|
authenticatedUser = 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>")
|
|
<< ", agent: " << request[http::field::user_agent]; //operator[] - Returns the value for a field, or "" if it does not exist.
|
|
|
|
Defer addRespCode ([&response, start, &logMsg]() {
|
|
logMsg << ", status: " << response.result() << ") took "
|
|
<< ch::duration_cast<ch::milliseconds>(ch::steady_clock::now() - start).count() << "ms.";
|
|
});
|
|
|
|
if (!HandleAccessControl(*m_Stream, request, response, yc)) {
|
|
break;
|
|
}
|
|
|
|
if (!EnsureAcceptHeader(*m_Stream, request, response, yc)) {
|
|
break;
|
|
}
|
|
|
|
if (!EnsureAuthenticatedUser(*m_Stream, request, authenticatedUser, response, yc)) {
|
|
break;
|
|
}
|
|
|
|
if (!EnsureValidBody(*m_Stream, buf, parser, authenticatedUser, response, m_ShuttingDown, yc)) {
|
|
break;
|
|
}
|
|
|
|
m_Seen = std::numeric_limits<decltype(m_Seen)>::max();
|
|
|
|
if (!ProcessRequest(*m_Stream, request, authenticatedUser, response, *this, m_HasStartedStreaming, yc)) {
|
|
break;
|
|
}
|
|
|
|
if (request.version() != 11 || request[http::field::connection] == "close") {
|
|
break;
|
|
}
|
|
}
|
|
} catch (const std::exception& ex) {
|
|
if (!m_ShuttingDown) {
|
|
Log(LogWarning, "HttpServerConnection")
|
|
<< "Exception while processing HTTP request from " << m_PeerAddress << ": " << ex.what();
|
|
}
|
|
}
|
|
|
|
Disconnect();
|
|
}
|
|
|
|
void HttpServerConnection::CheckLiveness(boost::asio::yield_context yc)
|
|
{
|
|
boost::system::error_code ec;
|
|
|
|
for (;;) {
|
|
m_CheckLivenessTimer.expires_from_now(boost::posix_time::seconds(5));
|
|
m_CheckLivenessTimer.async_wait(yc[ec]);
|
|
|
|
if (m_ShuttingDown) {
|
|
break;
|
|
}
|
|
|
|
if (m_Seen < Utility::GetTime() - 10) {
|
|
Log(LogInformation, "HttpServerConnection")
|
|
<< "No messages for HTTP connection have been received in the last 10 seconds.";
|
|
|
|
Disconnect();
|
|
break;
|
|
}
|
|
}
|
|
}
|