From c262c701d95246ab2a9f2e4911254e3bad46d433 Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Thu, 7 Feb 2019 16:33:59 +0100
Subject: [PATCH 01/67] Require Boost v1.66.0+
---
CMakeLists.txt | 2 +-
doc/21-development.md | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d94eb79ff..359fa16a2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,7 +1,7 @@
# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
cmake_minimum_required(VERSION 2.8.8)
-set(BOOST_MIN_VERSION "1.53.0")
+set(BOOST_MIN_VERSION "1.66.0")
project(icinga2)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
diff --git a/doc/21-development.md b/doc/21-development.md
index aa50f0097..72bca8f33 100644
--- a/doc/21-development.md
+++ b/doc/21-development.md
@@ -1095,8 +1095,8 @@ Icinga application using a dist tarball (including notes for distributions):
- SUSE: libopenssl-devel (for SLES 11: libopenssl1-devel)
- Debian/Ubuntu: libssl-dev
- Alpine: libressl-dev
-* Boost library and header files >= 1.53.0
- - RHEL/Fedora: boost153-devel
+* Boost library and header files >= 1.66.0
+ - RHEL/Fedora: boost166-devel
- Debian/Ubuntu: libboost-all-dev
- Alpine: boost-dev
* GNU bison (bison)
From f38c68a4c6d5ab590b679361bf7105bf800f048e Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Fri, 8 Feb 2019 11:51:22 +0100
Subject: [PATCH 02/67] Require Boost context, coroutine and date_time
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 359fa16a2..a3ee2e503 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -132,7 +132,7 @@ if(LOGROTATE_HAS_SU)
set(LOGROTATE_USE_SU "\n\tsu ${ICINGA2_USER} ${ICINGA2_GROUP}")
endif()
-find_package(Boost ${BOOST_MIN_VERSION} COMPONENTS thread system program_options regex REQUIRED)
+find_package(Boost ${BOOST_MIN_VERSION} COMPONENTS context coroutine date_time thread system program_options regex REQUIRED)
link_directories(${Boost_LIBRARY_DIRS})
include_directories(${Boost_INCLUDE_DIRS})
From 7c7c5e28f5f4354aa86cd0469d64ce6a3a80ef7e Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Tue, 12 Feb 2019 11:47:03 +0100
Subject: [PATCH 03/67] Implement LazyInit
---
lib/base/CMakeLists.txt | 1 +
lib/base/lazy-init.hpp | 89 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 90 insertions(+)
create mode 100644 lib/base/lazy-init.hpp
diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt
index 6d7661080..9e04de723 100644
--- a/lib/base/CMakeLists.txt
+++ b/lib/base/CMakeLists.txt
@@ -35,6 +35,7 @@ set(base_SOURCES
function.cpp function.hpp function-ti.hpp function-script.cpp functionwrapper.hpp
initialize.cpp initialize.hpp
json.cpp json.hpp json-script.cpp
+ lazy-init.hpp
library.cpp library.hpp
loader.cpp loader.hpp
logger.cpp logger.hpp logger-ti.hpp
diff --git a/lib/base/lazy-init.hpp b/lib/base/lazy-init.hpp
new file mode 100644
index 000000000..dd20a80e5
--- /dev/null
+++ b/lib/base/lazy-init.hpp
@@ -0,0 +1,89 @@
+/******************************************************************************
+ * Icinga 2 *
+ * Copyright (C) 2012-2018 Icinga Development Team (https://icinga.com/) *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License *
+ * as published by the Free Software Foundation; either version 2 *
+ * of the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the Free Software Foundation *
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
+ ******************************************************************************/
+
+#ifndef LAZY_INIT
+#define LAZY_INIT
+
+#include
+#include
+#include
+#include
+
+namespace icinga
+{
+
+/**
+ * Lazy object initialization abstraction inspired from
+ * .
+ *
+ * @ingroup base
+ */
+template
+class LazyInit
+{
+public:
+ inline
+ LazyInit(std::function initializer = []() { return T(); }) : m_Initializer(std::move(initializer))
+ {
+ m_Underlying.store(nullptr, std::memory_order_release);
+ }
+
+ LazyInit(const LazyInit&) = delete;
+ LazyInit(LazyInit&&) = delete;
+ LazyInit& operator=(const LazyInit&) = delete;
+ LazyInit& operator=(LazyInit&&) = delete;
+
+ inline
+ ~LazyInit()
+ {
+ auto ptr (m_Underlying.load(std::memory_order_acquire));
+
+ if (ptr != nullptr) {
+ delete ptr;
+ }
+ }
+
+ inline
+ T& Get()
+ {
+ auto ptr (m_Underlying.load(std::memory_order_acquire));
+
+ if (ptr == nullptr) {
+ std::unique_lock lock (m_Mutex);
+
+ ptr = m_Underlying.load(std::memory_order_acquire);
+
+ if (ptr == nullptr) {
+ ptr = new T(m_Initializer());
+ m_Underlying.store(ptr, std::memory_order_release);
+ }
+ }
+
+ return *ptr;
+ }
+
+private:
+ std::function m_Initializer;
+ std::mutex m_Mutex;
+ std::atomic m_Underlying;
+};
+
+}
+
+#endif /* LAZY_INIT */
From c547e9a86398ca524625c35d3815fb476f8f1311 Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Fri, 8 Feb 2019 10:05:24 +0100
Subject: [PATCH 04/67] Implement basic I/O engine
---
lib/base/CMakeLists.txt | 1 +
lib/base/io-engine.cpp | 96 +++++++++++++++++++++++++++++++++++++++++
lib/base/io-engine.hpp | 85 ++++++++++++++++++++++++++++++++++++
3 files changed, 182 insertions(+)
create mode 100644 lib/base/io-engine.cpp
create mode 100644 lib/base/io-engine.hpp
diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt
index 9e04de723..9260b31ed 100644
--- a/lib/base/CMakeLists.txt
+++ b/lib/base/CMakeLists.txt
@@ -34,6 +34,7 @@ set(base_SOURCES
filelogger.cpp filelogger.hpp filelogger-ti.hpp
function.cpp function.hpp function-ti.hpp function-script.cpp functionwrapper.hpp
initialize.cpp initialize.hpp
+ io-engine.cpp io-engine.hpp
json.cpp json.hpp json-script.cpp
lazy-init.hpp
library.cpp library.hpp
diff --git a/lib/base/io-engine.cpp b/lib/base/io-engine.cpp
new file mode 100644
index 000000000..e1aeeb094
--- /dev/null
+++ b/lib/base/io-engine.cpp
@@ -0,0 +1,96 @@
+/******************************************************************************
+ * Icinga 2 *
+ * Copyright (C) 2012-2018 Icinga Development Team (https://icinga.com/) *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License *
+ * as published by the Free Software Foundation; either version 2 *
+ * of the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the Free Software Foundation *
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
+ ******************************************************************************/
+
+#include "base/exception.hpp"
+#include "base/io-engine.hpp"
+#include "base/lazy-init.hpp"
+#include "base/logger.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace icinga;
+
+CpuBoundWork::CpuBoundWork(boost::asio::yield_context yc)
+{
+ auto& ioEngine (IoEngine::Get());
+
+ for (;;) {
+ auto availableSlots (ioEngine.m_CpuBoundSemaphore.fetch_sub(1));
+
+ if (availableSlots < 1) {
+ ioEngine.m_CpuBoundSemaphore.fetch_add(1);
+ ioEngine.m_AlreadyExpiredTimer.async_wait(yc);
+ continue;
+ }
+
+ break;
+ }
+}
+
+CpuBoundWork::~CpuBoundWork()
+{
+ IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1);
+}
+
+LazyInit> IoEngine::m_Instance ([]() { return std::unique_ptr(new IoEngine()); });
+
+IoEngine& IoEngine::Get()
+{
+ return *m_Instance.Get();
+}
+
+boost::asio::io_service& IoEngine::GetIoService()
+{
+ return m_IoService;
+}
+
+IoEngine::IoEngine() : m_IoService(), m_KeepAlive(m_IoService), m_AlreadyExpiredTimer(m_IoService)
+{
+ m_AlreadyExpiredTimer.expires_at(boost::posix_time::neg_infin);
+
+ auto concurrency (std::thread::hardware_concurrency());
+
+ if (concurrency < 2) {
+ m_CpuBoundSemaphore.store(1);
+ } else {
+ m_CpuBoundSemaphore.store(concurrency - 1u);
+ }
+
+ for (auto i (std::thread::hardware_concurrency()); i; --i) {
+ std::thread(&IoEngine::RunEventLoop, this).detach();
+ }
+}
+
+void IoEngine::RunEventLoop()
+{
+ for (;;) {
+ try {
+ m_IoService.run();
+
+ break;
+ } catch (const std::exception& e) {
+ Log(LogCritical, "IoEngine", "Exception during I/O operation!");
+ Log(LogDebug, "IoEngine") << "Exception during I/O operation: " << DiagnosticInformation(e);
+ }
+ }
+}
diff --git a/lib/base/io-engine.hpp b/lib/base/io-engine.hpp
new file mode 100644
index 000000000..df84df9ce
--- /dev/null
+++ b/lib/base/io-engine.hpp
@@ -0,0 +1,85 @@
+/******************************************************************************
+ * Icinga 2 *
+ * Copyright (C) 2012-2018 Icinga Development Team (https://icinga.com/) *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License *
+ * as published by the Free Software Foundation; either version 2 *
+ * of the License, or (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, write to the Free Software Foundation *
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
+ ******************************************************************************/
+
+#ifndef IO_ENGINE_H
+#define IO_ENGINE_H
+
+/**
+ * Boost.Coroutine2 (the successor of Boost.Coroutine)
+ * (1) doesn't even exist in old Boost versions and
+ * (2) isn't supported by ASIO, yet.
+ */
+#define BOOST_COROUTINES_NO_DEPRECATION_WARNING 1
+
+#include "base/lazy-init.hpp"
+#include
+#include
+#include
+#include
+#include
+
+/**
+ * Scope lock for CPU-bound work done in an I/O thread
+ *
+ * @ingroup base
+ */
+class CpuBoundWork
+{
+public:
+ CpuBoundWork(boost::asio::yield_context yc);
+ CpuBoundWork(const CpuBoundWork&) = delete;
+ CpuBoundWork(CpuBoundWork&&) = delete;
+ CpuBoundWork& operator=(const CpuBoundWork&) = delete;
+ CpuBoundWork& operator=(CpuBoundWork&&) = delete;
+ ~CpuBoundWork();
+};
+
+/**
+ * Async I/O engine
+ *
+ * @ingroup base
+ */
+class IoEngine
+{
+ friend CpuBoundWork;
+
+public:
+ IoEngine(const IoEngine&) = delete;
+ IoEngine(IoEngine&&) = delete;
+ IoEngine& operator=(const IoEngine&) = delete;
+ IoEngine& operator=(IoEngine&&) = delete;
+
+ static IoEngine& Get();
+
+ boost::asio::io_service& GetIoService();
+
+private:
+ IoEngine();
+
+ void RunEventLoop();
+
+ static LazyInit> m_Instance;
+
+ boost::asio::io_service m_IoService;
+ boost::asio::io_service::work m_KeepAlive;
+ boost::asio::deadline_timer m_AlreadyExpiredTimer;
+ std::atomic_uint_fast32_t m_CpuBoundSemaphore;
+};
+
+#endif /* IO_ENGINE_H */
From e4f3422b3a69dd0181de531740c7f008d34dd7cb Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Fri, 8 Feb 2019 11:43:47 +0100
Subject: [PATCH 05/67] ApiListener: listen(2) via Boost ASIO
---
lib/remote/apilistener.cpp | 62 ++++++++++++++++++++------------------
lib/remote/apilistener.hpp | 5 ++-
2 files changed, 34 insertions(+), 33 deletions(-)
diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp
index a26846d38..d8e1832ee 100644
--- a/lib/remote/apilistener.cpp
+++ b/lib/remote/apilistener.cpp
@@ -7,6 +7,7 @@
#include "remote/jsonrpc.hpp"
#include "remote/apifunction.hpp"
#include "base/convert.hpp"
+#include "base/io-engine.hpp"
#include "base/netstring.hpp"
#include "base/json.hpp"
#include "base/configtype.hpp"
@@ -18,7 +19,12 @@
#include "base/context.hpp"
#include "base/statsfunction.hpp"
#include "base/exception.hpp"
+#include
+#include
+#include
+#include
#include
+#include
using namespace icinga;
@@ -326,6 +332,10 @@ bool ApiListener::IsMaster() const
*/
bool ApiListener::AddListener(const String& node, const String& service)
{
+ namespace asio = boost::asio;
+ namespace ip = asio::ip;
+ using ip::tcp;
+
ObjectLock olock(this);
std::shared_ptr sslContext = m_SSLContext;
@@ -335,47 +345,40 @@ bool ApiListener::AddListener(const String& node, const String& service)
return false;
}
- TcpSocket::Ptr server = new TcpSocket();
+ auto& io (IoEngine::Get().GetIoService());
+ auto acceptor (std::make_shared(io));
try {
- server->Bind(node, service, AF_UNSPEC);
+ tcp::resolver resolver (io);
+ tcp::resolver::query query (node, service, tcp::resolver::query::passive);
+ auto endpoint (resolver.resolve(query)->endpoint());
+
+ acceptor->open(endpoint.protocol());
+ acceptor->set_option(ip::v6_only(false));
+ acceptor->set_option(tcp::acceptor::reuse_address(true));
+ acceptor->bind(endpoint);
} catch (const std::exception&) {
Log(LogCritical, "ApiListener")
<< "Cannot bind TCP socket for host '" << node << "' on port '" << service << "'.";
return false;
}
+ acceptor->listen(INT_MAX);
+
+ auto localEndpoint (acceptor->local_endpoint());
+
Log(LogInformation, "ApiListener")
- << "Started new listener on '" << server->GetClientAddress() << "'";
+ << "Started new listener on '[" << localEndpoint.address() << "]:" << localEndpoint.port() << "'";
- std::thread thread(std::bind(&ApiListener::ListenerThreadProc, this, server));
- thread.detach();
+ asio::spawn(io, [acceptor](asio::yield_context yc) {
+ // TODO
+ });
- m_Servers.insert(server);
-
- UpdateStatusFile(server);
+ UpdateStatusFile(localEndpoint);
return true;
}
-void ApiListener::ListenerThreadProc(const Socket::Ptr& server)
-{
- Utility::SetThreadName("API Listener");
-
- server->Listen();
-
- for (;;) {
- try {
- Socket::Ptr client = server->Accept();
-
- /* Use dynamic thread pool with additional on demand resources with fast throughput. */
- EnqueueAsyncCallback(std::bind(&ApiListener::NewClientHandler, this, client, String(), RoleServer), LowLatencyScheduler);
- } catch (const std::exception&) {
- Log(LogCritical, "ApiListener", "Cannot accept new connection.");
- }
- }
-}
-
/**
* Creates a new JSON-RPC client and connects to the specified endpoint.
*
@@ -1513,14 +1516,13 @@ String ApiListener::GetFromZoneName(const Zone::Ptr& fromZone)
return fromZoneName;
}
-void ApiListener::UpdateStatusFile(TcpSocket::Ptr socket)
+void ApiListener::UpdateStatusFile(boost::asio::ip::tcp::endpoint localEndpoint)
{
String path = Configuration::CacheDir + "/api-state.json";
- std::pair details = socket->GetClientAddressDetails();
Utility::SaveJsonFile(path, 0644, new Dictionary({
- {"host", details.first},
- {"port", Convert::ToLong(details.second)}
+ {"host", String(localEndpoint.address().to_string())},
+ {"port", localEndpoint.port()}
}));
}
diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp
index 54b96dee5..96861d74b 100644
--- a/lib/remote/apilistener.hpp
+++ b/lib/remote/apilistener.hpp
@@ -14,6 +14,7 @@
#include "base/tcpsocket.hpp"
#include "base/tlsstream.hpp"
#include "base/threadpool.hpp"
+#include
#include
namespace icinga
@@ -106,7 +107,6 @@ protected:
private:
std::shared_ptr m_SSLContext;
- std::set m_Servers;
mutable boost::mutex m_AnonymousClientsLock;
mutable boost::mutex m_HttpClientsLock;
@@ -130,7 +130,6 @@ private:
void NewClientHandler(const Socket::Ptr& client, const String& hostname, ConnectionRole role);
void NewClientHandlerInternal(const Socket::Ptr& client, const String& hostname, ConnectionRole role);
- void ListenerThreadProc(const Socket::Ptr& server);
static ThreadPool& GetTP();
static void EnqueueAsyncCallback(const std::function& callback, SchedulerPolicy policy = DefaultScheduler);
@@ -154,7 +153,7 @@ private:
static void CopyCertificateFile(const String& oldCertPath, const String& newCertPath);
- void UpdateStatusFile(TcpSocket::Ptr socket);
+ void UpdateStatusFile(boost::asio::ip::tcp::endpoint localEndpoint);
void RemoveStatusFile();
/* filesync */
From 2615967e7f461b502c2394d1f80ff3be0948bc5b Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Fri, 8 Feb 2019 14:23:10 +0100
Subject: [PATCH 06/67] Make ApiListener#m_SSLContext a Boost ASIO SSL context
---
lib/base/tlsstream.cpp | 25 +++++++++++-
lib/base/tlsstream.hpp | 4 ++
lib/base/tlsutility.cpp | 83 +++++++++++++++++++++++++-------------
lib/base/tlsutility.hpp | 8 ++--
lib/remote/apilistener.cpp | 11 +++--
lib/remote/apilistener.hpp | 3 +-
6 files changed, 97 insertions(+), 37 deletions(-)
diff --git a/lib/base/tlsstream.cpp b/lib/base/tlsstream.cpp
index 5d7678417..129d0bc74 100644
--- a/lib/base/tlsstream.cpp
+++ b/lib/base/tlsstream.cpp
@@ -6,6 +6,7 @@
#include "base/logger.hpp"
#include "base/configuration.hpp"
#include "base/convert.hpp"
+#include
#include
#ifndef _WIN32
@@ -26,6 +27,28 @@ bool TlsStream::m_SSLIndexInitialized = false;
* @param sslContext The SSL context for the client.
*/
TlsStream::TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const std::shared_ptr& sslContext)
+ : TlsStream(socket, hostname, role, sslContext.get())
+{
+}
+
+/**
+ * Constructor for the TlsStream class.
+ *
+ * @param role The role of the client.
+ * @param sslContext The SSL context for the client.
+ */
+TlsStream::TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const std::shared_ptr& sslContext)
+ : TlsStream(socket, hostname, role, sslContext->native_handle())
+{
+}
+
+/**
+ * Constructor for the TlsStream class.
+ *
+ * @param role The role of the client.
+ * @param sslContext The SSL context for the client.
+ */
+TlsStream::TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, SSL_CTX* sslContext)
: SocketEvents(socket), m_Eof(false), m_HandshakeOK(false), m_VerifyOK(true), m_ErrorCode(0),
m_ErrorOccurred(false), m_Socket(socket), m_Role(role), m_SendQ(new FIFO()), m_RecvQ(new FIFO()),
m_CurrentAction(TlsActionNone), m_Retry(false), m_Shutdown(false)
@@ -33,7 +56,7 @@ TlsStream::TlsStream(const Socket::Ptr& socket, const String& hostname, Connecti
std::ostringstream msgbuf;
char errbuf[120];
- m_SSL = std::shared_ptr(SSL_new(sslContext.get()), SSL_free);
+ m_SSL = std::shared_ptr(SSL_new(sslContext), SSL_free);
if (!m_SSL) {
msgbuf << "SSL_new() failed with code " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\"";
diff --git a/lib/base/tlsstream.hpp b/lib/base/tlsstream.hpp
index 8af5fb58e..cd3d0abfa 100644
--- a/lib/base/tlsstream.hpp
+++ b/lib/base/tlsstream.hpp
@@ -9,6 +9,7 @@
#include "base/stream.hpp"
#include "base/tlsutility.hpp"
#include "base/fifo.hpp"
+#include
namespace icinga
{
@@ -32,6 +33,7 @@ public:
DECLARE_PTR_TYPEDEFS(TlsStream);
TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const std::shared_ptr& sslContext = MakeSSLContext());
+ TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const std::shared_ptr& sslContext);
~TlsStream() override;
Socket::Ptr GetSocket() const;
@@ -80,6 +82,8 @@ private:
static int m_SSLIndex;
static bool m_SSLIndexInitialized;
+ TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, SSL_CTX* sslContext);
+
void OnEvent(int revents) override;
void HandleError() const;
diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp
index 35f4d3ba5..57f8d1901 100644
--- a/lib/base/tlsutility.cpp
+++ b/lib/base/tlsutility.cpp
@@ -7,6 +7,7 @@
#include "base/utility.hpp"
#include "base/application.hpp"
#include "base/exception.hpp"
+#include
#include
namespace icinga
@@ -57,35 +58,23 @@ void InitializeOpenSSL()
l_SSLInitialized = true;
}
-/**
- * Initializes an SSL context using the specified certificates.
- *
- * @param pubkey The public key.
- * @param privkey The matching private key.
- * @param cakey CA certificate chain file.
- * @returns An SSL context.
- */
-std::shared_ptr MakeSSLContext(const String& pubkey, const String& privkey, const String& cakey)
+static void SetupSslContext(SSL_CTX *sslContext, const String& pubkey, const String& privkey, const String& cakey)
{
char errbuf[120];
- InitializeOpenSSL();
-
- std::shared_ptr sslContext = std::shared_ptr(SSL_CTX_new(SSLv23_method()), SSL_CTX_free);
-
long flags = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_CIPHER_SERVER_PREFERENCE;
#ifdef SSL_OP_NO_COMPRESSION
flags |= SSL_OP_NO_COMPRESSION;
#endif /* SSL_OP_NO_COMPRESSION */
- SSL_CTX_set_options(sslContext.get(), flags);
+ SSL_CTX_set_options(sslContext, flags);
- SSL_CTX_set_mode(sslContext.get(), SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
- SSL_CTX_set_session_id_context(sslContext.get(), (const unsigned char *)"Icinga 2", 8);
+ SSL_CTX_set_mode(sslContext, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ SSL_CTX_set_session_id_context(sslContext, (const unsigned char *)"Icinga 2", 8);
if (!pubkey.IsEmpty()) {
- if (!SSL_CTX_use_certificate_chain_file(sslContext.get(), pubkey.CStr())) {
+ if (!SSL_CTX_use_certificate_chain_file(sslContext, pubkey.CStr())) {
Log(LogCritical, "SSL")
<< "Error with public key file '" << pubkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\"";
BOOST_THROW_EXCEPTION(openssl_error()
@@ -96,7 +85,7 @@ std::shared_ptr MakeSSLContext(const String& pubkey, const String& priv
}
if (!privkey.IsEmpty()) {
- if (!SSL_CTX_use_PrivateKey_file(sslContext.get(), privkey.CStr(), SSL_FILETYPE_PEM)) {
+ if (!SSL_CTX_use_PrivateKey_file(sslContext, privkey.CStr(), SSL_FILETYPE_PEM)) {
Log(LogCritical, "SSL")
<< "Error with private key file '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\"";
BOOST_THROW_EXCEPTION(openssl_error()
@@ -105,7 +94,7 @@ std::shared_ptr MakeSSLContext(const String& pubkey, const String& priv
<< boost::errinfo_file_name(privkey));
}
- if (!SSL_CTX_check_private_key(sslContext.get())) {
+ if (!SSL_CTX_check_private_key(sslContext)) {
Log(LogCritical, "SSL")
<< "Error checking private key '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\"";
BOOST_THROW_EXCEPTION(openssl_error()
@@ -115,7 +104,7 @@ std::shared_ptr MakeSSLContext(const String& pubkey, const String& priv
}
if (!cakey.IsEmpty()) {
- if (!SSL_CTX_load_verify_locations(sslContext.get(), cakey.CStr(), nullptr)) {
+ if (!SSL_CTX_load_verify_locations(sslContext, cakey.CStr(), nullptr)) {
Log(LogCritical, "SSL")
<< "Error loading and verifying locations in ca key file '" << cakey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\"";
BOOST_THROW_EXCEPTION(openssl_error()
@@ -136,22 +125,60 @@ std::shared_ptr MakeSSLContext(const String& pubkey, const String& priv
<< boost::errinfo_file_name(cakey));
}
- SSL_CTX_set_client_CA_list(sslContext.get(), cert_names);
+ SSL_CTX_set_client_CA_list(sslContext, cert_names);
}
+}
+
+/**
+ * Initializes an SSL context using the specified certificates.
+ *
+ * @param pubkey The public key.
+ * @param privkey The matching private key.
+ * @param cakey CA certificate chain file.
+ * @returns An SSL context.
+ */
+std::shared_ptr MakeSSLContext(const String& pubkey, const String& privkey, const String& cakey)
+{
+ InitializeOpenSSL();
+
+ std::shared_ptr sslContext = std::shared_ptr(SSL_CTX_new(SSLv23_method()), SSL_CTX_free);
+
+ SetupSslContext(sslContext.get(), pubkey, privkey, cakey);
return sslContext;
}
+/**
+ * Initializes an SSL context using the specified certificates.
+ *
+ * @param pubkey The public key.
+ * @param privkey The matching private key.
+ * @param cakey CA certificate chain file.
+ * @returns An SSL context.
+ */
+std::shared_ptr MakeAsioSslContext(const String& pubkey, const String& privkey, const String& cakey)
+{
+ namespace ssl = boost::asio::ssl;
+
+ InitializeOpenSSL();
+
+ auto context (std::make_shared(ssl::context::sslv23));
+
+ SetupSslContext(context->native_handle(), pubkey, privkey, cakey);
+
+ return context;
+}
+
/**
* Set the cipher list to the specified SSL context.
* @param context The ssl context.
* @param cipherList The ciper list.
**/
-void SetCipherListToSSLContext(const std::shared_ptr& context, const String& cipherList)
+void SetCipherListToSSLContext(const std::shared_ptr& context, const String& cipherList)
{
char errbuf[256];
- if (SSL_CTX_set_cipher_list(context.get(), cipherList.CStr()) == 0) {
+ if (SSL_CTX_set_cipher_list(context->native_handle(), cipherList.CStr()) == 0) {
Log(LogCritical, "SSL")
<< "Cipher list '"
<< cipherList
@@ -171,9 +198,9 @@ void SetCipherListToSSLContext(const std::shared_ptr& context, const St
* @param context The ssl context.
* @param tlsProtocolmin The minimum TLS protocol version.
*/
-void SetTlsProtocolminToSSLContext(const std::shared_ptr& context, const String& tlsProtocolmin)
+void SetTlsProtocolminToSSLContext(const std::shared_ptr& context, const String& tlsProtocolmin)
{
- long flags = SSL_CTX_get_options(context.get());
+ long flags = SSL_CTX_get_options(context->native_handle());
flags |= SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3;
@@ -190,7 +217,7 @@ void SetTlsProtocolminToSSLContext(const std::shared_ptr& context, cons
if (tlsProtocolmin != SSL_TXT_TLSV1)
BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid TLS protocol version specified."));
- SSL_CTX_set_options(context.get(), flags);
+ SSL_CTX_set_options(context->native_handle(), flags);
}
/**
@@ -199,10 +226,10 @@ void SetTlsProtocolminToSSLContext(const std::shared_ptr& context, cons
* @param context The SSL context.
* @param crlPath The path to the CRL file.
*/
-void AddCRLToSSLContext(const std::shared_ptr& context, const String& crlPath)
+void AddCRLToSSLContext(const std::shared_ptr& context, const String& crlPath)
{
char errbuf[120];
- X509_STORE *x509_store = SSL_CTX_get_cert_store(context.get());
+ X509_STORE *x509_store = SSL_CTX_get_cert_store(context->native_handle());
X509_LOOKUP *lookup;
lookup = X509_STORE_add_lookup(x509_store, X509_LOOKUP_file());
diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp
index afe21f2e4..69b10786c 100644
--- a/lib/base/tlsutility.hpp
+++ b/lib/base/tlsutility.hpp
@@ -14,6 +14,7 @@
#include
#include
#include
+#include
#include
namespace icinga
@@ -21,9 +22,10 @@ namespace icinga
void InitializeOpenSSL();
std::shared_ptr MakeSSLContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String());
-void AddCRLToSSLContext(const std::shared_ptr& context, const String& crlPath);
-void SetCipherListToSSLContext(const std::shared_ptr& context, const String& cipherList);
-void SetTlsProtocolminToSSLContext(const std::shared_ptr& context, const String& tlsProtocolmin);
+std::shared_ptr MakeAsioSslContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String());
+void AddCRLToSSLContext(const std::shared_ptr& context, const String& crlPath);
+void SetCipherListToSSLContext(const std::shared_ptr& context, const String& cipherList);
+void SetTlsProtocolminToSSLContext(const std::shared_ptr& context, const String& tlsProtocolmin);
String GetCertificateCN(const std::shared_ptr& certificate);
std::shared_ptr GetX509Certificate(const String& pemfile);
int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile = String(), const String& certfile = String(), bool ca = false);
diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp
index d8e1832ee..d680cc76a 100644
--- a/lib/remote/apilistener.cpp
+++ b/lib/remote/apilistener.cpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -165,10 +166,12 @@ void ApiListener::OnConfigLoaded()
void ApiListener::UpdateSSLContext()
{
- std::shared_ptr context;
+ namespace ssl = boost::asio::ssl;
+
+ std::shared_ptr context;
try {
- context = MakeSSLContext(GetDefaultCertPath(), GetDefaultKeyPath(), GetDefaultCaPath());
+ context = MakeAsioSslContext(GetDefaultCertPath(), GetDefaultKeyPath(), GetDefaultCaPath());
} catch (const std::exception&) {
BOOST_THROW_EXCEPTION(ScriptError("Cannot make SSL context for cert path: '"
+ GetDefaultCertPath() + "' key path: '" + GetDefaultKeyPath() + "' ca path: '" + GetDefaultCaPath() + "'.", GetDebugInfo()));
@@ -338,7 +341,7 @@ bool ApiListener::AddListener(const String& node, const String& service)
ObjectLock olock(this);
- std::shared_ptr sslContext = m_SSLContext;
+ auto sslContext (m_SSLContext);
if (!sslContext) {
Log(LogCritical, "ApiListener", "SSL context is required for AddListener()");
@@ -389,7 +392,7 @@ void ApiListener::AddConnection(const Endpoint::Ptr& endpoint)
{
ObjectLock olock(this);
- std::shared_ptr sslContext = m_SSLContext;
+ auto sslContext (m_SSLContext);
if (!sslContext) {
Log(LogCritical, "ApiListener", "SSL context is required for AddConnection()");
diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp
index 96861d74b..1de66ed4f 100644
--- a/lib/remote/apilistener.hpp
+++ b/lib/remote/apilistener.hpp
@@ -15,6 +15,7 @@
#include "base/tlsstream.hpp"
#include "base/threadpool.hpp"
#include
+#include
#include
namespace icinga
@@ -106,7 +107,7 @@ protected:
void ValidateTlsHandshakeTimeout(const Lazy& lvalue, const ValidationUtils& utils) override;
private:
- std::shared_ptr m_SSLContext;
+ std::shared_ptr m_SSLContext;
mutable boost::mutex m_AnonymousClientsLock;
mutable boost::mutex m_HttpClientsLock;
From 720c53ab77756ed915c3c1830323f4480498a87d Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Fri, 8 Feb 2019 18:00:53 +0100
Subject: [PATCH 07/67] ApiListener: perform TLS handshake
---
lib/remote/apilistener.cpp | 95 ++++++++++++++++++++++++++++++++++++--
lib/remote/apilistener.hpp | 6 +++
2 files changed, 98 insertions(+), 3 deletions(-)
diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp
index d680cc76a..578e1a682 100644
--- a/lib/remote/apilistener.cpp
+++ b/lib/remote/apilistener.cpp
@@ -23,9 +23,13 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
+#include
+#include
using namespace icinga;
@@ -373,15 +377,36 @@ bool ApiListener::AddListener(const String& node, const String& service)
Log(LogInformation, "ApiListener")
<< "Started new listener on '[" << localEndpoint.address() << "]:" << localEndpoint.port() << "'";
- asio::spawn(io, [acceptor](asio::yield_context yc) {
- // TODO
- });
+ asio::spawn(io, [this, acceptor, sslContext](asio::yield_context yc) { ListenerCoroutineProc(yc, acceptor, sslContext); });
UpdateStatusFile(localEndpoint);
return true;
}
+void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const std::shared_ptr& server, const std::shared_ptr& sslContext)
+{
+ namespace asio = boost::asio;
+ namespace ssl = asio::ssl;
+ using asio::ip::tcp;
+
+ auto& io (server->get_io_service());
+ auto sslConn (std::make_shared>(io, *sslContext));
+
+ for (;;) {
+ try {
+ server->async_accept(sslConn->lowest_layer(), yc);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener") << "Cannot accept new connection: " << DiagnosticInformation(ex, false);
+ continue;
+ }
+
+ asio::spawn(io, [this, sslConn](asio::yield_context yc) { NewClientHandler(yc, sslConn, String(), RoleServer); });
+
+ sslConn = std::make_shared>(io, *sslContext);
+ }
+}
+
/**
* Creates a new JSON-RPC client and connects to the specified endpoint.
*
@@ -601,6 +626,70 @@ void ApiListener::NewClientHandlerInternal(const Socket::Ptr& client, const Stri
}
}
+void ApiListener::NewClientHandler(boost::asio::yield_context yc, const std::shared_ptr>& client, const String& hostname, ConnectionRole role)
+{
+ try {
+ NewClientHandlerInternal(yc, client, hostname, role);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Exception while handling new API client connection: " << DiagnosticInformation(ex, false);
+
+ Log(LogDebug, "ApiListener")
+ << "Exception while handling new API client connection: " << DiagnosticInformation(ex);
+ }
+}
+
+/**
+ * Processes a new client connection.
+ *
+ * @param client The new client.
+ */
+void ApiListener::NewClientHandlerInternal(boost::asio::yield_context yc, const std::shared_ptr>& client, const String& hostname, ConnectionRole role)
+{
+ namespace ssl = boost::asio::ssl;
+
+ String conninfo;
+
+ {
+ std::ostringstream conninfo_;
+
+ if (role == RoleClient) {
+ conninfo_ << "to";
+ } else {
+ conninfo_ << "from";
+ }
+
+ auto endpoint (client->lowest_layer().remote_endpoint());
+
+ conninfo_ << " [" << endpoint.address() << "]:" << endpoint.port();
+
+ conninfo = conninfo_.str();
+ }
+
+ client->set_verify_mode(ssl::verify_peer | ssl::verify_client_once);
+
+ if (role == RoleClient) {
+ String environmentName = Application::GetAppEnvironment();
+ String serverName = hostname;
+
+ if (!environmentName.IsEmpty())
+ serverName += ":" + environmentName;
+
+#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
+ if (!hostname.IsEmpty()) {
+ SSL_set_tlsext_host_name(client->native_handle(), serverName.CStr());
+ }
+#endif /* SSL_CTRL_SET_TLSEXT_HOSTNAME */
+ }
+
+ try {
+ client->async_handshake(role == RoleClient ? client->client : client->server, yc);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Client TLS handshake failed (" << conninfo << "): " << DiagnosticInformation(ex, false);
+ }
+}
+
void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoint::Ptr& endpoint, bool needSync)
{
Zone::Ptr eZone = endpoint->GetZone();
diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp
index 1de66ed4f..e8b578aba 100644
--- a/lib/remote/apilistener.hpp
+++ b/lib/remote/apilistener.hpp
@@ -15,7 +15,9 @@
#include "base/tlsstream.hpp"
#include "base/threadpool.hpp"
#include
+#include
#include
+#include
#include
namespace icinga
@@ -132,6 +134,10 @@ private:
void NewClientHandler(const Socket::Ptr& client, const String& hostname, ConnectionRole role);
void NewClientHandlerInternal(const Socket::Ptr& client, const String& hostname, ConnectionRole role);
+ void NewClientHandler(boost::asio::yield_context yc, const std::shared_ptr>& client, const String& hostname, ConnectionRole role);
+ void NewClientHandlerInternal(boost::asio::yield_context yc, const std::shared_ptr>& client, const String& hostname, ConnectionRole role);
+ void ListenerCoroutineProc(boost::asio::yield_context yc, const std::shared_ptr& server, const std::shared_ptr& sslContext);
+
static ThreadPool& GetTP();
static void EnqueueAsyncCallback(const std::function& callback, SchedulerPolicy policy = DefaultScheduler);
From 539855bac15485e3b00a80b35b43a0f310580076 Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Tue, 12 Feb 2019 14:56:25 +0100
Subject: [PATCH 08/67] ApiListener: verify peer
---
lib/remote/apilistener.cpp | 65 ++++++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp
index 578e1a682..4adb9035b 100644
--- a/lib/remote/apilistener.cpp
+++ b/lib/remote/apilistener.cpp
@@ -24,11 +24,14 @@
#include
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
+#include
#include
using namespace icinga;
@@ -668,6 +671,23 @@ void ApiListener::NewClientHandlerInternal(boost::asio::yield_context yc, const
client->set_verify_mode(ssl::verify_peer | ssl::verify_client_once);
+ bool verify_ok = false;
+ String verifyError;
+
+ client->set_verify_callback([&verify_ok, &verifyError](bool preverified, ssl::verify_context& ctx) {
+ verify_ok = preverified;
+
+ if (!preverified) {
+ std::ostringstream msgbuf;
+ int err = X509_STORE_CTX_get_error(ctx.native_handle());
+
+ msgbuf << "code " << err << ": " << X509_verify_cert_error_string(err);
+ verifyError = msgbuf.str();
+ }
+
+ return preverified;
+ });
+
if (role == RoleClient) {
String environmentName = Application::GetAppEnvironment();
String serverName = hostname;
@@ -687,6 +707,51 @@ void ApiListener::NewClientHandlerInternal(boost::asio::yield_context yc, const
} catch (const std::exception& ex) {
Log(LogCritical, "ApiListener")
<< "Client TLS handshake failed (" << conninfo << "): " << DiagnosticInformation(ex, false);
+ return;
+ }
+
+ std::shared_ptr cert (SSL_get_peer_certificate(client->native_handle()), X509_free);
+ String identity;
+ Endpoint::Ptr endpoint;
+
+ if (cert) {
+ try {
+ identity = GetCertificateCN(cert);
+ } catch (const std::exception&) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot get certificate common name from cert path: '" << GetDefaultCertPath() << "'.";
+ return;
+ }
+
+ if (!hostname.IsEmpty()) {
+ if (identity != hostname) {
+ Log(LogWarning, "ApiListener")
+ << "Unexpected certificate common name while connecting to endpoint '"
+ << hostname << "': got '" << identity << "'";
+ return;
+ } else if (!verify_ok) {
+ Log(LogWarning, "ApiListener")
+ << "Certificate validation failed for endpoint '" << hostname
+ << "': " << verifyError;
+ }
+ }
+
+ if (verify_ok) {
+ endpoint = Endpoint::GetByName(identity);
+ }
+
+ Log log(LogInformation, "ApiListener");
+
+ log << "New client connection for identity '" << identity << "' " << conninfo;
+
+ if (!verify_ok) {
+ log << " (certificate validation failed: " << verifyError << ")";
+ } else if (!endpoint) {
+ log << " (no Endpoint object found for identity)";
+ }
+ } else {
+ Log(LogInformation, "ApiListener")
+ << "New client connection " << conninfo << " (no client certificate)";
}
}
From e21956e26e7edc20d0b75c331f79405cd980867e Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Tue, 12 Feb 2019 14:56:47 +0100
Subject: [PATCH 09/67] ApiListener: detect protocol
---
lib/base/tlsstream.hpp | 32 +++++++++++++++++++
lib/remote/apilistener.cpp | 63 ++++++++++++++++++++++++++++++--------
lib/remote/apilistener.hpp | 5 ++-
3 files changed, 84 insertions(+), 16 deletions(-)
diff --git a/lib/base/tlsstream.hpp b/lib/base/tlsstream.hpp
index cd3d0abfa..cca8cb286 100644
--- a/lib/base/tlsstream.hpp
+++ b/lib/base/tlsstream.hpp
@@ -9,7 +9,12 @@
#include "base/stream.hpp"
#include "base/tlsutility.hpp"
#include "base/fifo.hpp"
+#include
+#include
+#include
+#include
#include
+#include
namespace icinga
{
@@ -94,6 +99,33 @@ private:
void CloseInternal(bool inDestructor);
};
+class AsioTlsStreamHack : public boost::asio::ssl::stream
+{
+public:
+ inline
+ AsioTlsStreamHack(std::pair& init)
+ : stream(*init.first, *init.second)
+ {
+ }
+};
+
+class AsioTlsStream : public boost::asio::buffered_stream
+{
+public:
+ inline
+ AsioTlsStream(boost::asio::io_service& ioService, boost::asio::ssl::context& sslContext)
+ : AsioTlsStream(std::make_pair(&ioService, &sslContext))
+ {
+ }
+
+private:
+ inline
+ AsioTlsStream(std::pair init)
+ : buffered_stream(init)
+ {
+ }
+};
+
}
#endif /* TLSSTREAM_H */
diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp
index 4adb9035b..3f882a3d1 100644
--- a/lib/remote/apilistener.cpp
+++ b/lib/remote/apilistener.cpp
@@ -19,13 +19,14 @@
#include "base/context.hpp"
#include "base/statsfunction.hpp"
#include "base/exception.hpp"
+#include
#include
#include
#include
#include
-#include
#include
#include
+#include
#include
#include
#include
@@ -390,11 +391,9 @@ bool ApiListener::AddListener(const String& node, const String& service)
void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const std::shared_ptr& server, const std::shared_ptr& sslContext)
{
namespace asio = boost::asio;
- namespace ssl = asio::ssl;
- using asio::ip::tcp;
auto& io (server->get_io_service());
- auto sslConn (std::make_shared>(io, *sslContext));
+ auto sslConn (std::make_shared(io, *sslContext));
for (;;) {
try {
@@ -406,7 +405,7 @@ void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const std
asio::spawn(io, [this, sslConn](asio::yield_context yc) { NewClientHandler(yc, sslConn, String(), RoleServer); });
- sslConn = std::make_shared>(io, *sslContext);
+ sslConn = std::make_shared(io, *sslContext);
}
}
@@ -629,7 +628,7 @@ void ApiListener::NewClientHandlerInternal(const Socket::Ptr& client, const Stri
}
}
-void ApiListener::NewClientHandler(boost::asio::yield_context yc, const std::shared_ptr>& client, const String& hostname, ConnectionRole role)
+void ApiListener::NewClientHandler(boost::asio::yield_context yc, const std::shared_ptr& client, const String& hostname, ConnectionRole role)
{
try {
NewClientHandlerInternal(yc, client, hostname, role);
@@ -647,9 +646,10 @@ void ApiListener::NewClientHandler(boost::asio::yield_context yc, const std::sha
*
* @param client The new client.
*/
-void ApiListener::NewClientHandlerInternal(boost::asio::yield_context yc, const std::shared_ptr>& client, const String& hostname, ConnectionRole role)
+void ApiListener::NewClientHandlerInternal(boost::asio::yield_context yc, const std::shared_ptr& client, const String& hostname, ConnectionRole role)
{
- namespace ssl = boost::asio::ssl;
+ namespace asio = boost::asio;
+ namespace ssl = asio::ssl;
String conninfo;
@@ -669,12 +669,14 @@ void ApiListener::NewClientHandlerInternal(boost::asio::yield_context yc, const
conninfo = conninfo_.str();
}
- client->set_verify_mode(ssl::verify_peer | ssl::verify_client_once);
+ auto& sslConn (client->next_layer());
+
+ sslConn.set_verify_mode(ssl::verify_peer | ssl::verify_client_once);
bool verify_ok = false;
String verifyError;
- client->set_verify_callback([&verify_ok, &verifyError](bool preverified, ssl::verify_context& ctx) {
+ sslConn.set_verify_callback([&verify_ok, &verifyError](bool preverified, ssl::verify_context& ctx) {
verify_ok = preverified;
if (!preverified) {
@@ -697,20 +699,20 @@ void ApiListener::NewClientHandlerInternal(boost::asio::yield_context yc, const
#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
if (!hostname.IsEmpty()) {
- SSL_set_tlsext_host_name(client->native_handle(), serverName.CStr());
+ SSL_set_tlsext_host_name(sslConn.native_handle(), serverName.CStr());
}
#endif /* SSL_CTRL_SET_TLSEXT_HOSTNAME */
}
try {
- client->async_handshake(role == RoleClient ? client->client : client->server, yc);
+ sslConn.async_handshake(role == RoleClient ? sslConn.client : sslConn.server, yc);
} catch (const std::exception& ex) {
Log(LogCritical, "ApiListener")
<< "Client TLS handshake failed (" << conninfo << "): " << DiagnosticInformation(ex, false);
return;
}
- std::shared_ptr cert (SSL_get_peer_certificate(client->native_handle()), X509_free);
+ std::shared_ptr cert (SSL_get_peer_certificate(sslConn.native_handle()), X509_free);
String identity;
Endpoint::Ptr endpoint;
@@ -753,6 +755,41 @@ void ApiListener::NewClientHandlerInternal(boost::asio::yield_context yc, const
Log(LogInformation, "ApiListener")
<< "New client connection " << conninfo << " (no client certificate)";
}
+
+ ClientType ctype;
+
+ if (role != RoleClient) {
+ {
+ boost::system::error_code ec;
+
+ if (client->async_fill(yc[ec]) == 0u) {
+ if (identity.IsEmpty()) {
+ Log(LogInformation, "ApiListener")
+ << "No data received on new API connection. "
+ << "Ensure that the remote endpoints are properly configured in a cluster setup.";
+ } else {
+ Log(LogWarning, "ApiListener")
+ << "No data received on new API connection for identity '" << identity << "'. "
+ << "Ensure that the remote endpoints are properly configured in a cluster setup.";
+ }
+
+ return;
+ }
+ }
+
+ char firstByte = 0;
+
+ {
+ asio::mutable_buffer firstByteBuf (&firstByte, 1);
+ client->peek(firstByteBuf);
+ }
+
+ if (firstByte >= '0' && firstByte <= '9') {
+ ctype = ClientJsonRpc;
+ } else {
+ ctype = ClientHttp;
+ }
+ }
}
void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoint::Ptr& endpoint, bool needSync)
diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp
index e8b578aba..093837493 100644
--- a/lib/remote/apilistener.hpp
+++ b/lib/remote/apilistener.hpp
@@ -17,7 +17,6 @@
#include
#include
#include
-#include
#include
namespace icinga
@@ -134,8 +133,8 @@ private:
void NewClientHandler(const Socket::Ptr& client, const String& hostname, ConnectionRole role);
void NewClientHandlerInternal(const Socket::Ptr& client, const String& hostname, ConnectionRole role);
- void NewClientHandler(boost::asio::yield_context yc, const std::shared_ptr>& client, const String& hostname, ConnectionRole role);
- void NewClientHandlerInternal(boost::asio::yield_context yc, const std::shared_ptr>& client, const String& hostname, ConnectionRole role);
+ void NewClientHandler(boost::asio::yield_context yc, const std::shared_ptr& client, const String& hostname, ConnectionRole role);
+ void NewClientHandlerInternal(boost::asio::yield_context yc, const std::shared_ptr& client, const String& hostname, ConnectionRole role);
void ListenerCoroutineProc(boost::asio::yield_context yc, const std::shared_ptr& server, const std::shared_ptr& sslContext);
static ThreadPool& GetTP();
From 2d7714802d661e02e3c8ac1f7410cc7ffe63780a Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Thu, 14 Feb 2019 13:10:04 +0100
Subject: [PATCH 10/67] Allow CpuBoundWork to be done before end of scope
---
lib/base/io-engine.cpp | 14 +++++++++++++-
lib/base/io-engine.hpp | 5 +++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/lib/base/io-engine.cpp b/lib/base/io-engine.cpp
index e1aeeb094..482d57176 100644
--- a/lib/base/io-engine.cpp
+++ b/lib/base/io-engine.cpp
@@ -31,6 +31,7 @@
using namespace icinga;
CpuBoundWork::CpuBoundWork(boost::asio::yield_context yc)
+ : m_Done(false)
{
auto& ioEngine (IoEngine::Get());
@@ -49,7 +50,18 @@ CpuBoundWork::CpuBoundWork(boost::asio::yield_context yc)
CpuBoundWork::~CpuBoundWork()
{
- IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1);
+ if (!m_Done) {
+ IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1);
+ }
+}
+
+void CpuBoundWork::Done()
+{
+ if (!m_Done) {
+ IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1);
+
+ m_Done = true;
+ }
}
LazyInit> IoEngine::m_Instance ([]() { return std::unique_ptr(new IoEngine()); });
diff --git a/lib/base/io-engine.hpp b/lib/base/io-engine.hpp
index df84df9ce..efeb56f99 100644
--- a/lib/base/io-engine.hpp
+++ b/lib/base/io-engine.hpp
@@ -48,6 +48,11 @@ public:
CpuBoundWork& operator=(const CpuBoundWork&) = delete;
CpuBoundWork& operator=(CpuBoundWork&&) = delete;
~CpuBoundWork();
+
+ void Done();
+
+private:
+ bool m_Done;
};
/**
From fc22cbaf096d6031abe9fab2fee62c30b521a9e9 Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Thu, 14 Feb 2019 13:12:36 +0100
Subject: [PATCH 11/67] Add HttpUtility::SendJsonBody() overload for
Boost/Beast
---
lib/remote/httputility.cpp | 10 ++++++++++
lib/remote/httputility.hpp | 2 ++
2 files changed, 12 insertions(+)
diff --git a/lib/remote/httputility.cpp b/lib/remote/httputility.cpp
index 043f3cf9e..f7fef7713 100644
--- a/lib/remote/httputility.cpp
+++ b/lib/remote/httputility.cpp
@@ -5,6 +5,7 @@
#include "base/logger.hpp"
#include
";
- response.WriteBody(msg.CStr(), msg.GetLength());
- response.Finish();
-
- m_CurrentRequest.~HttpRequest();
- new (&m_CurrentRequest) HttpRequest(m_Stream);
-
- m_Stream->Shutdown();
-
- return false;
- } catch (const std::exception& ex) {
- response.SetStatus(500, "Internal Server Error");
- String msg = "Internal Server Error
" + DiagnosticInformation(ex) + "
";
- response.WriteBody(msg.CStr(), msg.GetLength());
- response.Finish();
-
- m_CurrentRequest.~HttpRequest();
- new (&m_CurrentRequest) HttpRequest(m_Stream);
-
- m_Stream->Shutdown();
-
- return false;
- }
- return res;
- }
-
- if (!m_CurrentRequest.CompleteHeaderCheck) {
- m_CurrentRequest.CompleteHeaderCheck = true;
- if (!ManageHeaders(response)) {
- m_CurrentRequest.~HttpRequest();
- new (&m_CurrentRequest) HttpRequest(m_Stream);
-
- m_Stream->Shutdown();
-
- return false;
- }
- }
-
- if (!m_CurrentRequest.CompleteBody) {
- try {
- res = m_CurrentRequest.ParseBody(m_Context, false);
- } catch (const std::invalid_argument& ex) {
- response.SetStatus(400, "Bad Request");
- String msg = String("Bad Request
") + ex.what() + "
";
- response.WriteBody(msg.CStr(), msg.GetLength());
- response.Finish();
-
- m_CurrentRequest.~HttpRequest();
- new (&m_CurrentRequest) HttpRequest(m_Stream);
-
- m_Stream->Shutdown();
-
- return false;
- } catch (const std::exception& ex) {
- response.SetStatus(500, "Internal Server Error");
- String msg = "Internal Server Error
" + DiagnosticInformation(ex) + "
";
- response.WriteBody(msg.CStr(), msg.GetLength());
- response.Finish();
-
- m_CurrentRequest.~HttpRequest();
- new (&m_CurrentRequest) HttpRequest(m_Stream);
-
- m_Stream->Shutdown();
-
- return false;
- }
- return res;
- }
-
- m_RequestQueue.Enqueue(std::bind(&HttpServerConnection::ProcessMessageAsync,
- HttpServerConnection::Ptr(this), m_CurrentRequest, response, m_AuthenticatedUser));
-
- m_Seen = Utility::GetTime();
- m_PendingRequests++;
-
- m_CurrentRequest.~HttpRequest();
- new (&m_CurrentRequest) HttpRequest(m_Stream);
-
- return false;
-}
-
-bool HttpServerConnection::ManageHeaders(HttpResponse& response)
-{
- if (m_CurrentRequest.Headers->Get("expect") == "100-continue") {
- String continueResponse = "HTTP/1.1 100 Continue\r\n\r\n";
- m_Stream->Write(continueResponse.CStr(), continueResponse.GetLength());
- }
-
- /* client_cn matched. */
- if (m_ApiUser)
- m_AuthenticatedUser = m_ApiUser;
- else
- m_AuthenticatedUser = ApiUser::GetByAuthHeader(m_CurrentRequest.Headers->Get("authorization"));
-
- String requestUrl = m_CurrentRequest.RequestUrl->Format();
-
- Log(LogInformation, "HttpServerConnection")
- << "Request: " << m_CurrentRequest.RequestMethod << " " << requestUrl
- << " (from " << m_PeerAddress << ")"
- << ", user: " << (m_AuthenticatedUser ? m_AuthenticatedUser->GetName() : "") << ")";
-
- ApiListener::Ptr listener = ApiListener::GetInstance();
-
- if (!listener)
- return false;
-
- Array::Ptr headerAllowOrigin = listener->GetAccessControlAllowOrigin();
-
- if (headerAllowOrigin && headerAllowOrigin->GetLength() != 0) {
- String origin = m_CurrentRequest.Headers->Get("origin");
- {
- ObjectLock olock(headerAllowOrigin);
-
- for (const String& allowedOrigin : headerAllowOrigin) {
- if (allowedOrigin == origin)
- response.AddHeader("Access-Control-Allow-Origin", origin);
- }
+ http::async_read_header(stream, buf, parser, yc);
+ } catch (const boost::system::system_error& ex) {
+ /**
+ * Unfortunately there's no way to tell an HTTP protocol error
+ * from an error on a lower layer:
+ *
+ *
+ */
+ throw std::invalid_argument(ex.what());
}
- response.AddHeader("Access-Control-Allow-Credentials", "true");
-
- String accessControlRequestMethodHeader = m_CurrentRequest.Headers->Get("access-control-request-method");
-
- if (m_CurrentRequest.RequestMethod == "OPTIONS" && !accessControlRequestMethodHeader.IsEmpty()) {
- response.SetStatus(200, "OK");
-
- response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
- response.AddHeader("Access-Control-Allow-Headers", "Authorization, X-HTTP-Method-Override");
-
- String msg = "Preflight OK";
- response.WriteBody(msg.CStr(), msg.GetLength());
-
- response.Finish();
- return false;
+ switch (parser.get().version()) {
+ case 10:
+ case 11:
+ break;
+ default:
+ throw std::invalid_argument("Unsupported HTTP version");
}
- }
+ } catch (const std::invalid_argument& ex) {
+ response.result(http::status::bad_request);
+ response.set(http::field::content_type, "text/html");
+ response.body() = String("Bad Request
") + ex.what() + "
";
+ response.set(http::field::content_length, response.body().size());
+ response.set(http::field::connection, "close");
- if (m_CurrentRequest.RequestMethod != "GET" && m_CurrentRequest.Headers->Get("accept") != "application/json") {
- response.SetStatus(400, "Wrong Accept header");
- response.AddHeader("Content-Type", "text/html");
- String msg = "Accept header is missing or not set to 'application/json'.
";
- response.WriteBody(msg.CStr(), msg.GetLength());
- response.Finish();
- return false;
- }
-
- if (!m_AuthenticatedUser) {
- Log(LogWarning, "HttpServerConnection")
- << "Unauthorized request: " << m_CurrentRequest.RequestMethod << " " << requestUrl;
-
- response.SetStatus(401, "Unauthorized");
- response.AddHeader("WWW-Authenticate", "Basic realm=\"Icinga 2\"");
-
- if (m_CurrentRequest.Headers->Get("accept") == "application/json") {
- Dictionary::Ptr result = new Dictionary({
- { "error", 401 },
- { "status", "Unauthorized. Please check your user credentials." }
- });
-
- HttpUtility::SendJsonBody(response, nullptr, result);
- } else {
- response.AddHeader("Content-Type", "text/html");
- String msg = "Unauthorized. Please check your user credentials.
";
- response.WriteBody(msg.CStr(), msg.GetLength());
- }
-
- response.Finish();
- return false;
- }
-
- static const size_t defaultContentLengthLimit = 1 * 1024 * 1024;
- size_t maxSize = defaultContentLengthLimit;
-
- Array::Ptr permissions = m_AuthenticatedUser->GetPermissions();
-
- if (permissions) {
- ObjectLock olock(permissions);
-
- for (const Value& permissionInfo : permissions) {
- String permission;
-
- if (permissionInfo.IsObjectType())
- permission = static_cast(permissionInfo)->Get("permission");
- else
- permission = permissionInfo;
-
- static std::vector> 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;
- }
- }
- }
-
- size_t contentLength = m_CurrentRequest.Headers->Get("content-length");
-
- if (contentLength > maxSize) {
- response.SetStatus(400, "Bad Request");
- String msg = String("Content length exceeded maximum
");
- response.WriteBody(msg.CStr(), msg.GetLength());
- response.Finish();
+ http::async_write(stream, response, yc);
+ stream.async_flush(yc);
return false;
}
@@ -313,64 +101,319 @@ bool HttpServerConnection::ManageHeaders(HttpResponse& response)
return true;
}
-void HttpServerConnection::ProcessMessageAsync(HttpRequest& request, HttpResponse& response, const ApiUser::Ptr& user)
+static inline
+void HandleExpect100(
+ AsioTlsStream& stream,
+ boost::beast::http::request& request,
+ boost::asio::yield_context& yc
+)
{
- response.RebindRequest(request);
+ namespace http = boost::beast::http;
+
+ if (request[http::field::expect] == "100-continue") {
+ http::response 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& request,
+ boost::beast::http::response& response,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ auto listener (ApiListener::GetInstance());
+
+ if (listener) {
+ auto headerAllowOrigin (listener->GetAccessControlAllowOrigin());
+
+ if (headerAllowOrigin) {
+ CpuBoundWork allowOriginHeader (yc);
+
+ auto allowedOrigins (headerAllowOrigin->ToSet());
+
+ if (!allowedOrigins.empty()) {
+ auto& origin (request[http::field::origin]);
+
+ if (allowedOrigins.find(origin.to_string()) != allowedOrigins.end()) {
+ response.set(http::field::access_control_allow_origin, origin);
+ }
+
+ allowOriginHeader.Done();
+
+ 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, X-HTTP-Method-Override");
+ response.body() = "Preflight OK";
+ response.set(http::field::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& request,
+ boost::beast::http::response& 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() = "Accept header is missing or not set to 'application/json'.
";
+ response.set(http::field::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& request,
+ ApiUser::Ptr& authenticatedUser,
+ boost::beast::http::response& 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() = "Unauthorized. Please check your user credentials.
";
+ response.set(http::field::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& parser,
+ ApiUser::Ptr& authenticatedUser,
+ boost::beast::http::response& response,
+ 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()) {
+ permission = static_cast(permissionInfo)->Get("permission");
+ } else {
+ permission = permissionInfo;
+ }
+
+ static std::vector> 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);
+ }
try {
- HttpHandler::ProcessRequest(user, request, response);
+ http::async_read(stream, buf, parser, yc);
+ } catch (const boost::system::system_error& ex) {
+ /**
+ * Unfortunately there's no way to tell an HTTP protocol error
+ * from an error on a lower layer:
+ *
+ *
+ */
+
+ response.result(http::status::bad_request);
+ response.set(http::field::content_type, "text/html");
+ response.body() = String("Bad Request
") + ex.what() + "
";
+ response.set(http::field::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 ProcessRequest(
+ AsioTlsStream& stream,
+ boost::beast::http::request& request,
+ ApiUser::Ptr& authenticatedUser,
+ boost::beast::http::response& response,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ HttpUtility::SendJsonError(response, nullptr, 503, "Unhandled exception" , "");
+
+ http::async_write(stream, response, yc);
+ stream.async_flush(yc);
+}
+
+void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
+{
+ namespace beast = boost::beast;
+ namespace http = beast::http;
+
+ Defer removeHttpClient ([this, &yc]() {
+ auto listener (ApiListener::GetInstance());
+
+ if (listener) {
+ CpuBoundWork removeHttpClient (yc);
+
+ listener->RemoveHttpClient(this);
+ }
+ });
+
+ Defer shutdown ([this, &yc]() {
+ try {
+ m_Stream->next_layer().async_shutdown(yc);
+ } catch (...) {
+ // https://stackoverflow.com/questions/130117/throwing-exceptions-out-of-a-destructor
+ }
+ });
+
+ try {
+ beast::flat_buffer buf;
+
+ for (;;) {
+ http::parser parser;
+ http::response response;
+
+ parser.header_limit(1024 * 1024);
+
+ response.set(http::field::server, l_ServerHeader);
+
+ if (!EnsureValidHeaders(*m_Stream, buf, parser, response, yc)) {
+ break;
+ }
+
+ 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) {
+ CpuBoundWork fetchingAuthenticatedUser (yc);
+
+ authenticatedUser = ApiUser::GetByAuthHeader(request[http::field::authorization].to_string());
+ }
+
+ Log(LogInformation, "HttpServerConnection")
+ << "Request: " << request.method_string() << ' ' << request.target()
+ << " (from " << m_PeerAddress
+ << "), user: " << (authenticatedUser ? authenticatedUser->GetName() : "") << ')';
+
+ 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, yc)) {
+ break;
+ }
+
+ ProcessRequest(*m_Stream, request, authenticatedUser, response, yc);
+
+ if (request.version() != 11 || request[http::field::connection] == "close") {
+ break;
+ }
+ }
} catch (const std::exception& ex) {
Log(LogCritical, "HttpServerConnection")
- << "Unhandled exception while processing Http request: " << DiagnosticInformation(ex);
- HttpUtility::SendJsonError(response, nullptr, 503, "Unhandled exception" , DiagnosticInformation(ex));
- }
-
- response.Finish();
- m_PendingRequests--;
-}
-
-void HttpServerConnection::DataAvailableHandler()
-{
- bool close = false;
-
- if (!m_Stream->IsEof()) {
- boost::recursive_mutex::scoped_try_lock lock(m_DataHandlerMutex);
- if (!lock.owns_lock()) {
- Log(LogNotice, "HttpServerConnection", "Unable to process available data, they're already being processed in another thread");
- return;
- }
-
- try {
- while (ProcessMessage())
- ; /* empty loop body */
- } catch (const std::exception& ex) {
- Log(LogWarning, "HttpServerConnection")
- << "Error while reading Http request: " << DiagnosticInformation(ex);
-
- close = true;
- }
- } else
- close = true;
-
- if (close)
- Disconnect();
-}
-
-void HttpServerConnection::CheckLiveness()
-{
- if (m_Seen < Utility::GetTime() - 10 && m_PendingRequests == 0 && m_Stream->IsEof()) {
- Log(LogInformation, "HttpServerConnection")
- << "No messages for Http connection have been received in the last 10 seconds.";
- Disconnect();
+ << "Unhandled exception while processing HTTP request: " << DiagnosticInformation(ex);
}
}
-
-void HttpServerConnection::TimeoutTimerHandler()
-{
- ApiListener::Ptr listener = ApiListener::GetInstance();
-
- for (const HttpServerConnection::Ptr& client : listener->GetHttpClients()) {
- client->CheckLiveness();
- }
-}
-
diff --git a/lib/remote/httpserverconnection.hpp b/lib/remote/httpserverconnection.hpp
index e27ba839c..3fdbeef50 100644
--- a/lib/remote/httpserverconnection.hpp
+++ b/lib/remote/httpserverconnection.hpp
@@ -3,12 +3,11 @@
#ifndef HTTPSERVERCONNECTION_H
#define HTTPSERVERCONNECTION_H
-#include "remote/httprequest.hpp"
-#include "remote/httpresponse.hpp"
#include "remote/apiuser.hpp"
+#include "base/string.hpp"
#include "base/tlsstream.hpp"
-#include "base/workqueue.hpp"
-#include
+#include
+#include
namespace icinga
{
@@ -23,39 +22,16 @@ class HttpServerConnection final : public Object
public:
DECLARE_PTR_TYPEDEFS(HttpServerConnection);
- HttpServerConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream);
+ HttpServerConnection(const String& identity, bool authenticated, const std::shared_ptr& stream);
void Start();
- ApiUser::Ptr GetApiUser() const;
- bool IsAuthenticated() const;
- TlsStream::Ptr GetStream() const;
-
- void Disconnect();
-
private:
ApiUser::Ptr m_ApiUser;
- ApiUser::Ptr m_AuthenticatedUser;
- TlsStream::Ptr m_Stream;
- double m_Seen;
- HttpRequest m_CurrentRequest;
- boost::recursive_mutex m_DataHandlerMutex;
- WorkQueue m_RequestQueue;
- int m_PendingRequests;
+ std::shared_ptr m_Stream;
String m_PeerAddress;
- StreamReadContext m_Context;
-
- bool ProcessMessage();
- void DataAvailableHandler();
-
- static void StaticInitialize();
- static void TimeoutTimerHandler();
- void CheckLiveness();
-
- bool ManageHeaders(HttpResponse& response);
-
- void ProcessMessageAsync(HttpRequest& request, HttpResponse& response, const ApiUser::Ptr&);
+ void ProcessMessages(boost::asio::yield_context yc);
};
}
From 9ae1d732af3052865c8bd2b4d1bafd90df0ee682 Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov"
Date: Thu, 14 Feb 2019 17:27:17 +0100
Subject: [PATCH 14/67] HttpServerConnection: actually handle requests
---
lib/remote/httphandler.cpp | 20 +++++++++++++-------
lib/remote/httphandler.hpp | 16 ++++++++++++++--
lib/remote/httpserverconnection.cpp | 15 ++++++++++++++-
lib/remote/httputility.cpp | 17 ++++++-----------
lib/remote/httputility.hpp | 4 +++-
5 files changed, 50 insertions(+), 22 deletions(-)
diff --git a/lib/remote/httphandler.cpp b/lib/remote/httphandler.cpp
index bf8bed37d..d6ea3426b 100644
--- a/lib/remote/httphandler.cpp
+++ b/lib/remote/httphandler.cpp
@@ -5,6 +5,7 @@
#include "base/singleton.hpp"
#include "base/exception.hpp"
#include
+#include
using namespace icinga;
@@ -44,11 +45,17 @@ void HttpHandler::Register(const Url::Ptr& url, const HttpHandler::Ptr& handler)
handlers->Add(handler);
}
-void HttpHandler::ProcessRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response)
+void HttpHandler::ProcessRequest(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request& request,
+ boost::beast::http::response& response
+)
{
Dictionary::Ptr node = m_UrlTree;
std::vector handlers;
- const std::vector& path = request.RequestUrl->GetPath();
+
+ Url::Ptr url = new Url(request.target().to_string());
+ auto& path (url->GetPath());
for (std::vector::size_type i = 0; i <= path.size(); i++) {
Array::Ptr current_handlers = node->Get("handlers");
@@ -81,7 +88,7 @@ void HttpHandler::ProcessRequest(const ApiUser::Ptr& user, HttpRequest& request,
Dictionary::Ptr params;
try {
- params = HttpUtility::FetchRequestParameters(request);
+ params = HttpUtility::FetchRequestParameters(url, request.body());
} catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 400, "Invalid request body: " + DiagnosticInformation(ex, false));
return;
@@ -89,16 +96,15 @@ void HttpHandler::ProcessRequest(const ApiUser::Ptr& user, HttpRequest& request,
bool processed = false;
for (const HttpHandler::Ptr& handler : handlers) {
- if (handler->HandleRequest(user, request, response, params)) {
+ if (handler->HandleRequest(user, request, url, response, params)) {
processed = true;
break;
}
}
if (!processed) {
- String path = boost::algorithm::join(request.RequestUrl->GetPath(), "/");
- HttpUtility::SendJsonError(response, params, 404, "The requested path '" + path +
- "' could not be found or the request method is not valid for this path.");
+ HttpUtility::SendJsonError(response, params, 404, "The requested path '" + boost::algorithm::join(path, "/") +
+ "' could not be found or the request method is not valid for this path.");
return;
}
}
diff --git a/lib/remote/httphandler.hpp b/lib/remote/httphandler.hpp
index 95ddd0b49..7e74329e0 100644
--- a/lib/remote/httphandler.hpp
+++ b/lib/remote/httphandler.hpp
@@ -4,10 +4,12 @@
#define HTTPHANDLER_H
#include "remote/i2-remote.hpp"
+#include "remote/url.hpp"
#include "remote/httpresponse.hpp"
#include "remote/apiuser.hpp"
#include "base/registry.hpp"
#include
+#include
namespace icinga
{
@@ -22,10 +24,20 @@ class HttpHandler : public Object
public:
DECLARE_PTR_TYPEDEFS(HttpHandler);
- virtual bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) = 0;
+ virtual bool HandleRequest(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request& request,
+ const Url::Ptr& url,
+ boost::beast::http::response& response,
+ const Dictionary::Ptr& params
+ ) = 0;
static void Register(const Url::Ptr& url, const HttpHandler::Ptr& handler);
- static void ProcessRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response);
+ static void ProcessRequest(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request& request,
+ boost::beast::http::response& response
+ );
private:
static Dictionary::Ptr m_UrlTree;
diff --git a/lib/remote/httpserverconnection.cpp b/lib/remote/httpserverconnection.cpp
index 707c1a635..87db3da5c 100644
--- a/lib/remote/httpserverconnection.cpp
+++ b/lib/remote/httpserverconnection.cpp
@@ -321,7 +321,20 @@ void ProcessRequest(
{
namespace http = boost::beast::http;
- HttpUtility::SendJsonError(response, nullptr, 503, "Unhandled exception" , "");
+ try {
+ CpuBoundWork handlingRequest (yc);
+
+ HttpHandler::ProcessRequest(authenticatedUser, request, response);
+ } catch (const std::exception& ex) {
+ http::response response;
+
+ HttpUtility::SendJsonError(response, nullptr, 500, "Unhandled exception" , DiagnosticInformation(ex));
+
+ http::async_write(stream, response, yc);
+ stream.async_flush(yc);
+
+ return;
+ }
http::async_write(stream, response, yc);
stream.async_flush(yc);
diff --git a/lib/remote/httputility.cpp b/lib/remote/httputility.cpp
index fa54d9c89..c97297cd0 100644
--- a/lib/remote/httputility.cpp
+++ b/lib/remote/httputility.cpp
@@ -1,28 +1,23 @@
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
#include "remote/httputility.hpp"
+#include "remote/url.hpp"
#include "base/json.hpp"
#include "base/logger.hpp"
#include