diff --git a/.travis.yml b/.travis.yml index 23405135f..03f43f032 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,50 +1,42 @@ -dist: trusty +dist: xenial sudo: false language: cpp cache: ccache -env: - global: - # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created - # via the "travis encrypt" command using the project repo's public key - - secure: "eOnFdiRhB7VUZY7Of4Ff0px93HRWGcD4fXCPiy8V2OC2ER98CYCVw7PKt2Is6i/yTveFTps1kObOo0T03aUT8y/xeBy/wMuJYk1d6mVgmSXOjxcxjQVTUh4J+xB+k/R6FoP2dirNDbvSayCj9Fi9toN9hQHMM8oAZOZfiKmYTJc=" - -before_install: - - echo -n | openssl s_client -connect scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca- - addons: - apt_packages: - - libboost-all-dev - - flex - - bison - - libssl-dev - - libpq-dev - - libmysqlclient-dev - - libedit-dev - - libwxbase3.0-dev - - libwxgtk3.0-dev - coverity_scan: - project: - name: "Icinga/icinga2" - notification_email: icinga2@icinga.com - build_command_prepend: "cmake . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/tmp/icinga2 -DICINGA2_PLUGINDIR=/tmp/icinga2/sbin -DICINGA2_UNITY_BUILD=ON" - build_command: "make -j 2" - branch_pattern: coverity_scan - + apt: + sources: + - sourceline: 'deb http://packages.icinga.com/ubuntu icinga-xenial main' + key_url: 'https://packages.icinga.com/icinga.key' + packages: + - libboost1.67-icinga-all-dev + - flex + - bison + - libssl-dev + - libpq-dev + - libmysqlclient-dev + - libedit-dev before_script: - - if [ "$COVERITY_SCAN_BRANCH" != 1 ]; then - mkdir build && - cd build && - cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/tmp/icinga2 -DICINGA2_PLUGINDIR=/tmp/icinga2/sbin; - fi + - arch=$(uname -m) + - mkdir build + - cd build + - > + cmake .. + -DCMAKE_BUILD_TYPE=Debug + -DICINGA2_UNITY_BUILD=Off + -DCMAKE_INSTALL_PREFIX=/tmp/icinga2 + -DICINGA2_PLUGINDIR=/tmp/icinga2/sbin + -DBoost_NO_BOOST_CMAKE=TRUE + -DBoost_NO_SYSTEM_PATHS=TRUE + -DBOOST_LIBRARYDIR=/usr/lib/${arch}-linux-gnu/icinga-boost + -DBOOST_INCLUDEDIR=/usr/include/icinga-boost + -DCMAKE_INSTALL_RPATH=/usr/lib/${arch}-linux-gnu/icinga-boost script: - - if [ "$COVERITY_SCAN_BRANCH" != 1 ]; then - make && - make test && - make install && - /tmp/icinga2/sbin/icinga2 --version && - /tmp/icinga2/sbin/icinga2 daemon -C -DRunAsUser=$(id -u -n) -DRunAsGroup=$(id -g -n); - fi + - make + - make test + - make install + - /tmp/icinga2/sbin/icinga2 --version + - /tmp/icinga2/sbin/icinga2 daemon -C -DRunAsUser=$(id -u -n) -DRunAsGroup=$(id -g -n) diff --git a/CMakeLists.txt b/CMakeLists.txt index d94eb79ff..888b462a3 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") @@ -132,7 +132,12 @@ 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) + +# Boost.Coroutine2 (the successor of Boost.Coroutine) +# (1) doesn't even exist in old Boost versions and +# (2) isn't supported by ASIO, yet. +add_definitions(-DBOOST_COROUTINES_NO_DEPRECATION_WARNING) link_directories(${Boost_LIBRARY_DIRS}) include_directories(${Boost_INCLUDE_DIRS}) @@ -190,6 +195,7 @@ if(WIN32) endif() set(CMAKE_MACOSX_RPATH 1) +set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_RPATH};${CMAKE_INSTALL_FULL_LIBDIR}/icinga2") if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Qunused-arguments -fcolor-diagnostics") diff --git a/appveyor.yml b/appveyor.yml index 9d1150f72..48fb507b0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,8 +7,9 @@ platform: x64 environment: CMAKE_GENERATOR: "Visual Studio 15 2017 Win64" VSCMD_VER: 15.0 - BOOST_ROOT: 'C:\Libraries\boost_1_65_1' - BOOST_LIBRARYDIR: 'C:\Libraries\boost_1_65_1\lib64-msvc-14.1' + # https://www.appveyor.com/docs/windows-images-software/#boost + BOOST_ROOT: 'C:\Libraries\boost_1_66_0' + BOOST_LIBRARYDIR: 'C:\Libraries\boost_1_66_0\lib64-msvc-14.1' BISON_BINARY: 'C:\ProgramData\chocolatey\lib\winflexbison3\tools\win_bison.exe' FLEX_BINARY: 'C:\ProgramData\chocolatey\lib\winflexbison3\tools\win_flex.exe' CMAKE_BUILD_TYPE: Debug diff --git a/doc/09-object-types.md b/doc/09-object-types.md index f3f69976f..e79861588 100644 --- a/doc/09-object-types.md +++ b/doc/09-object-types.md @@ -57,7 +57,7 @@ Configuration Attributes: ca\_path | String | **Deprecated.** Path to the CA certificate file. ticket\_salt | String | **Optional.** Private key for [CSR auto-signing](06-distributed-monitoring.md#distributed-monitoring-setup-csr-auto-signing). **Required** for a signing master instance. crl\_path | String | **Optional.** Path to the CRL file. - bind\_host | String | **Optional.** The IP address the api listener should be bound to. Defaults to `0.0.0.0`. + bind\_host | String | **Optional.** The IP address the api listener should be bound to. If not specified, the ApiListener is bound to `::` and listens for both IPv4 and IPv6 connections. bind\_port | Number | **Optional.** The port the api listener should be bound to. Defaults to `5665`. accept\_config | Boolean | **Optional.** Accept zone configuration. Defaults to `false`. accept\_commands | Boolean | **Optional.** Accept remote commands. Defaults to `false`. 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) diff --git a/icinga-app/CMakeLists.txt b/icinga-app/CMakeLists.txt index d3ecfe8ab..ee3443b28 100644 --- a/icinga-app/CMakeLists.txt +++ b/icinga-app/CMakeLists.txt @@ -68,7 +68,6 @@ target_link_libraries(icinga-app ${base_DEPS}) set_target_properties ( icinga-app PROPERTIES - INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2 FOLDER Bin OUTPUT_NAME icinga2 ) diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt index 6d7661080..9260b31ed 100644 --- a/lib/base/CMakeLists.txt +++ b/lib/base/CMakeLists.txt @@ -34,7 +34,9 @@ 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 loader.cpp loader.hpp logger.cpp logger.hpp logger-ti.hpp diff --git a/lib/base/io-engine.cpp b/lib/base/io-engine.cpp new file mode 100644 index 000000000..b70050552 --- /dev/null +++ b/lib/base/io-engine.cpp @@ -0,0 +1,162 @@ +/****************************************************************************** + * 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 +#include + +using namespace icinga; + +CpuBoundWork::CpuBoundWork(boost::asio::yield_context yc) + : m_Done(false) +{ + 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() +{ + 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; + } +} + +IoBoundWorkSlot::IoBoundWorkSlot(boost::asio::yield_context yc) + : yc(yc) +{ + IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1); +} + +IoBoundWorkSlot::~IoBoundWorkSlot() +{ + 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; + } +} + +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_Threads(decltype(m_Threads)::size_type(std::thread::hardware_concurrency() * 2u)), m_AlreadyExpiredTimer(m_IoService) +{ + m_AlreadyExpiredTimer.expires_at(boost::posix_time::neg_infin); + m_CpuBoundSemaphore.store(std::thread::hardware_concurrency() * 3u / 2u); + + for (auto& thread : m_Threads) { + thread = std::thread(&IoEngine::RunEventLoop, this); + } +} + +IoEngine::~IoEngine() +{ + for (auto& thread : m_Threads) { + m_IoService.post([]() { + throw TerminateIoThread(); + }); + } + + for (auto& thread : m_Threads) { + thread.join(); + } +} + +void IoEngine::RunEventLoop() +{ + for (;;) { + try { + m_IoService.run(); + + break; + } catch (const TerminateIoThread&) { + break; + } catch (const std::exception& e) { + Log(LogCritical, "IoEngine", "Exception during I/O operation!"); + Log(LogDebug, "IoEngine") << "Exception during I/O operation: " << DiagnosticInformation(e); + } + } +} + +AsioConditionVariable::AsioConditionVariable(boost::asio::io_service& io, bool init) + : m_Timer(io) +{ + m_Timer.expires_at(init ? boost::posix_time::neg_infin : boost::posix_time::pos_infin); +} + +void AsioConditionVariable::Set() +{ + m_Timer.expires_at(boost::posix_time::neg_infin); +} + +void AsioConditionVariable::Clear() +{ + m_Timer.expires_at(boost::posix_time::pos_infin); +} + +void AsioConditionVariable::Wait(boost::asio::yield_context yc) +{ + boost::system::error_code ec; + m_Timer.async_wait(yc[ec]); +} diff --git a/lib/base/io-engine.hpp b/lib/base/io-engine.hpp new file mode 100644 index 000000000..ebb498818 --- /dev/null +++ b/lib/base/io-engine.hpp @@ -0,0 +1,135 @@ +/****************************************************************************** + * 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 + +#include "base/lazy-init.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace icinga +{ + +/** + * 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(); + + void Done(); + +private: + bool m_Done; +}; + +/** + * Scope break for CPU-bound work done in an I/O thread + * + * @ingroup base + */ +class IoBoundWorkSlot +{ +public: + IoBoundWorkSlot(boost::asio::yield_context yc); + IoBoundWorkSlot(const IoBoundWorkSlot&) = delete; + IoBoundWorkSlot(IoBoundWorkSlot&&) = delete; + IoBoundWorkSlot& operator=(const IoBoundWorkSlot&) = delete; + IoBoundWorkSlot& operator=(IoBoundWorkSlot&&) = delete; + ~IoBoundWorkSlot(); + +private: + boost::asio::yield_context yc; +}; + +/** + * Async I/O engine + * + * @ingroup base + */ +class IoEngine +{ + friend CpuBoundWork; + friend IoBoundWorkSlot; + +public: + IoEngine(const IoEngine&) = delete; + IoEngine(IoEngine&&) = delete; + IoEngine& operator=(const IoEngine&) = delete; + IoEngine& operator=(IoEngine&&) = delete; + ~IoEngine(); + + 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; + std::vector m_Threads; + boost::asio::deadline_timer m_AlreadyExpiredTimer; + std::atomic_int_fast32_t m_CpuBoundSemaphore; +}; + +class TerminateIoThread : public std::exception +{ +}; + +/** + * Condition variable which doesn't block I/O threads + * + * @ingroup base + */ +class AsioConditionVariable +{ +public: + AsioConditionVariable(boost::asio::io_service& io, bool init = false); + + void Set(); + void Clear(); + void Wait(boost::asio::yield_context yc); + +private: + boost::asio::deadline_timer m_Timer; +}; + +} + +#endif /* IO_ENGINE_H */ 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 */ diff --git a/lib/base/netstring.cpp b/lib/base/netstring.cpp index 7fad3453c..489a8b40d 100644 --- a/lib/base/netstring.cpp +++ b/lib/base/netstring.cpp @@ -2,7 +2,15 @@ #include "base/netstring.hpp" #include "base/debug.hpp" +#include "base/tlsstream.hpp" +#include +#include #include +#include +#include +#include +#include +#include using namespace icinga; @@ -110,6 +118,108 @@ size_t NetString::WriteStringToStream(const Stream::Ptr& stream, const String& s return msg.GetLength(); } +/** + * Reads data from a stream in netstring format. + * + * @param stream The stream to read from. + * @returns The String that has been read from the IOQueue. + * @exception invalid_argument The input stream is invalid. + * @see https://github.com/PeterScott/netstring-c/blob/master/netstring.c + */ +String NetString::ReadStringFromStream(const std::shared_ptr& stream, + boost::asio::yield_context yc, ssize_t maxMessageLength) +{ + namespace asio = boost::asio; + + size_t len = 0; + bool leadingZero = false; + + for (uint_fast8_t readBytes = 0;; ++readBytes) { + char byte = 0; + + { + asio::mutable_buffer byteBuf (&byte, 1); + asio::async_read(*stream, byteBuf, yc); + } + + if (isdigit(byte)) { + if (readBytes == 9) { + BOOST_THROW_EXCEPTION(std::invalid_argument("Length specifier must not exceed 9 characters")); + } + + if (leadingZero) { + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (leading zero)")); + } + + len = len * 10u + size_t(byte - '0'); + + if (!readBytes && byte == '0') { + leadingZero = true; + } + } else if (byte == ':') { + if (!readBytes) { + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (no length specifier)")); + } + + break; + } else { + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (missing :)")); + } + } + + if (maxMessageLength >= 0 && len > maxMessageLength) { + std::stringstream errorMessage; + errorMessage << "Max data length exceeded: " << (maxMessageLength / 1024) << " KB"; + + BOOST_THROW_EXCEPTION(std::invalid_argument(errorMessage.str())); + } + + String payload; + + if (len) { + payload.Append(len, 0); + + asio::mutable_buffer payloadBuf (&*payload.Begin(), payload.GetLength()); + asio::async_read(*stream, payloadBuf, yc); + } + + char trailer = 0; + + { + asio::mutable_buffer trailerBuf (&trailer, 1); + asio::async_read(*stream, trailerBuf, yc); + } + + if (trailer != ',') { + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid NetString (missing ,)")); + } + + return std::move(payload); +} + +/** + * Writes data into a stream using the netstring format and returns bytes written. + * + * @param stream The stream. + * @param str The String that is to be written. + * + * @return The amount of bytes written. + */ +size_t NetString::WriteStringToStream(const std::shared_ptr& stream, const String& str, boost::asio::yield_context yc) +{ + namespace asio = boost::asio; + + std::ostringstream msgbuf; + WriteStringToStream(msgbuf, str); + + String msg = msgbuf.str(); + asio::const_buffer msgBuf (msg.CStr(), msg.GetLength()); + + asio::async_write(*stream, msgBuf, yc); + + return msg.GetLength(); +} + /** * Writes data into a stream using the netstring format. * diff --git a/lib/base/netstring.hpp b/lib/base/netstring.hpp index 10ff4e2fc..f84eac7a3 100644 --- a/lib/base/netstring.hpp +++ b/lib/base/netstring.hpp @@ -5,6 +5,9 @@ #include "base/i2-base.hpp" #include "base/stream.hpp" +#include "base/tlsstream.hpp" +#include +#include namespace icinga { @@ -23,7 +26,10 @@ class NetString public: static StreamReadStatus ReadStringFromStream(const Stream::Ptr& stream, String *message, StreamReadContext& context, bool may_wait = false, ssize_t maxMessageLength = -1); + static String ReadStringFromStream(const std::shared_ptr& stream, + boost::asio::yield_context yc, ssize_t maxMessageLength = -1); static size_t WriteStringToStream(const Stream::Ptr& stream, const String& message); + static size_t WriteStringToStream(const std::shared_ptr& stream, const String& message, boost::asio::yield_context yc); static void WriteStringToStream(std::ostream& stream, const String& message); private: diff --git a/lib/base/tcpsocket.hpp b/lib/base/tcpsocket.hpp index 4f8a2a0f0..069288aad 100644 --- a/lib/base/tcpsocket.hpp +++ b/lib/base/tcpsocket.hpp @@ -5,6 +5,8 @@ #include "base/i2-base.hpp" #include "base/socket.hpp" +#include +#include namespace icinga { @@ -25,6 +27,35 @@ public: void Connect(const String& node, const String& service); }; +template +void Connect(Socket& socket, const String& node, const String& service, boost::asio::yield_context yc) +{ + using boost::asio::ip::tcp; + + tcp::resolver resolver (socket.get_io_service()); + tcp::resolver::query query (node, service); + auto result (resolver.async_resolve(query, yc)); + auto current (result.begin()); + + for (;;) { + try { + socket.open(current->endpoint().protocol()); + socket.set_option(tcp::socket::keep_alive(true)); + socket.async_connect(current->endpoint(), yc); + + break; + } catch (const std::exception&) { + if (++current == result.end()) { + throw; + } + + if (socket.is_open()) { + socket.close(); + } + } + } +} + } #endif /* TCPSOCKET_H */ diff --git a/lib/base/tlsstream.cpp b/lib/base/tlsstream.cpp index 5d7678417..38913f28a 100644 --- a/lib/base/tlsstream.cpp +++ b/lib/base/tlsstream.cpp @@ -1,12 +1,20 @@ /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ +#include "base/application.hpp" #include "base/tlsstream.hpp" #include "base/utility.hpp" #include "base/exception.hpp" #include "base/logger.hpp" #include "base/configuration.hpp" #include "base/convert.hpp" +#include +#include +#include #include +#include +#include +#include +#include #ifndef _WIN32 # include @@ -26,6 +34,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 +63,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) << "\""; @@ -424,3 +454,46 @@ Socket::Ptr TlsStream::GetSocket() const { return m_Socket; } + +bool UnbufferedAsioTlsStream::IsVerifyOK() const +{ + return m_VerifyOK; +} + +String UnbufferedAsioTlsStream::GetVerifyError() const +{ + return m_VerifyError; +} + +void UnbufferedAsioTlsStream::BeforeHandshake(handshake_type type) +{ + namespace ssl = boost::asio::ssl; + + set_verify_mode(ssl::verify_peer | ssl::verify_client_once); + + set_verify_callback([this](bool preverified, ssl::verify_context& ctx) { + if (!preverified) { + m_VerifyOK = false; + + std::ostringstream msgbuf; + int err = X509_STORE_CTX_get_error(ctx.native_handle()); + + msgbuf << "code " << err << ": " << X509_verify_cert_error_string(err); + m_VerifyError = msgbuf.str(); + } + + return true; + }); + +#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME + if (type == client && !m_Hostname.IsEmpty()) { + String environmentName = Application::GetAppEnvironment(); + String serverName = m_Hostname; + + if (!environmentName.IsEmpty()) + serverName += ":" + environmentName; + + SSL_set_tlsext_host_name(native_handle(), serverName.CStr()); + } +#endif /* SSL_CTRL_SET_TLSEXT_HOSTNAME */ +} diff --git a/lib/base/tlsstream.hpp b/lib/base/tlsstream.hpp index 8af5fb58e..6156a3d2f 100644 --- a/lib/base/tlsstream.hpp +++ b/lib/base/tlsstream.hpp @@ -9,6 +9,12 @@ #include "base/stream.hpp" #include "base/tlsutility.hpp" #include "base/fifo.hpp" +#include +#include +#include +#include +#include +#include namespace icinga { @@ -32,6 +38,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 +87,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; @@ -90,6 +99,70 @@ private: void CloseInternal(bool inDestructor); }; +struct UnbufferedAsioTlsStreamParams +{ + boost::asio::io_service& IoService; + boost::asio::ssl::context& SslContext; + const String& Hostname; +}; + +typedef boost::asio::ssl::stream AsioTcpTlsStream; + +class UnbufferedAsioTlsStream : public AsioTcpTlsStream +{ +public: + inline + UnbufferedAsioTlsStream(UnbufferedAsioTlsStreamParams& init) + : stream(init.IoService, init.SslContext), m_VerifyOK(true), m_Hostname(init.Hostname) + { + } + + bool IsVerifyOK() const; + String GetVerifyError() const; + + template + inline + auto async_handshake(handshake_type type, Args&&... args) -> decltype(((AsioTcpTlsStream*)nullptr)->async_handshake(type, std::forward(args)...)) + { + BeforeHandshake(type); + + return AsioTcpTlsStream::async_handshake(type, std::forward(args)...); + } + + template + inline + auto handshake(handshake_type type, Args&&... args) -> decltype(((AsioTcpTlsStream*)nullptr)->handshake(type, std::forward(args)...)) + { + BeforeHandshake(type); + + return AsioTcpTlsStream::handshake(type, std::forward(args)...); + } + +private: + bool m_VerifyOK; + String m_VerifyError; + String m_Hostname; + + void BeforeHandshake(handshake_type type); +}; + +class AsioTlsStream : public boost::asio::buffered_stream +{ +public: + inline + AsioTlsStream(boost::asio::io_service& ioService, boost::asio::ssl::context& sslContext, const String& hostname = String()) + : AsioTlsStream(UnbufferedAsioTlsStreamParams{ioService, sslContext, hostname}) + { + } + +private: + inline + AsioTlsStream(UnbufferedAsioTlsStreamParams init) + : buffered_stream(init) + { + } +}; + } #endif /* TLSSTREAM_H */ 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/actionshandler.cpp b/lib/remote/actionshandler.cpp index cc1cba8de..251f8a82e 100644 --- a/lib/remote/actionshandler.cpp +++ b/lib/remote/actionshandler.cpp @@ -12,15 +12,26 @@ using namespace icinga; REGISTER_URLHANDLER("/v1/actions", ActionsHandler); -bool ActionsHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool ActionsHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() != 3) + namespace http = boost::beast::http; + + if (url->GetPath().size() != 3) return false; - if (request.RequestMethod != "POST") + if (request.method() != http::verb::post) return false; - String actionName = request.RequestUrl->GetPath()[2]; + String actionName = url->GetPath()[2]; ApiAction::Ptr action = ApiAction::GetByName(actionName); @@ -81,17 +92,15 @@ bool ActionsHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& reques } int statusCode = 500; - String statusMessage = "No action executed successfully"; for (const Dictionary::Ptr& res : results) { if (res->Contains("code") && res->Get("code") == 200) { statusCode = 200; - statusMessage = "OK"; break; } } - response.SetStatus(statusCode, statusMessage); + response.result(statusCode); Dictionary::Ptr result = new Dictionary({ { "results", new Array(std::move(results)) } diff --git a/lib/remote/actionshandler.hpp b/lib/remote/actionshandler.hpp index 7affb0814..9c0463035 100644 --- a/lib/remote/actionshandler.hpp +++ b/lib/remote/actionshandler.hpp @@ -13,8 +13,16 @@ class ActionsHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(ActionsHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/apilistener-configsync.cpp b/lib/remote/apilistener-configsync.cpp index d4caff91a..e06d1d887 100644 --- a/lib/remote/apilistener-configsync.cpp +++ b/lib/remote/apilistener-configsync.cpp @@ -323,7 +323,7 @@ void ApiListener::UpdateConfigObject(const ConfigObject::Ptr& object, const Mess #endif /* I2_DEBUG */ if (client) - JsonRpc::SendMessage(client->GetStream(), message); + client->SendMessage(message); else { Zone::Ptr target = static_pointer_cast(object->GetZone()); @@ -373,7 +373,7 @@ void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const Mess #endif /* I2_DEBUG */ if (client) - JsonRpc::SendMessage(client->GetStream(), message); + client->SendMessage(message); else { Zone::Ptr target = static_pointer_cast(object->GetZone()); diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index a26846d38..de6e754c0 100644 --- a/lib/remote/apilistener.cpp +++ b/lib/remote/apilistener.cpp @@ -7,6 +7,8 @@ #include "remote/jsonrpc.hpp" #include "remote/apifunction.hpp" #include "base/convert.hpp" +#include "base/defer.hpp" +#include "base/io-engine.hpp" #include "base/netstring.hpp" #include "base/json.hpp" #include "base/configtype.hpp" @@ -18,7 +20,19 @@ #include "base/context.hpp" #include "base/statsfunction.hpp" #include "base/exception.hpp" +#include "base/tcpsocket.hpp" +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include using namespace icinga; @@ -95,22 +109,6 @@ void ApiListener::CopyCertificateFile(const String& oldCertPath, const String& n } } -/** - * Returns the API thread pool. - * - * @returns The API thread pool. - */ -ThreadPool& ApiListener::GetTP() -{ - static ThreadPool tp; - return tp; -} - -void ApiListener::EnqueueAsyncCallback(const std::function& callback, SchedulerPolicy policy) -{ - GetTP().Post(callback, policy); -} - void ApiListener::OnConfigLoaded() { if (m_Instance) @@ -159,10 +157,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())); @@ -326,52 +326,95 @@ 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; + auto sslContext (m_SSLContext); if (!sslContext) { Log(LogCritical, "ApiListener", "SSL context is required for AddListener()"); 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); - } catch (const std::exception&) { + tcp::resolver resolver (io); + tcp::resolver::query query (node, service, tcp::resolver::query::passive); + + auto result (resolver.resolve(query)); + auto current (result.begin()); + + for (;;) { + try { + acceptor->open(current->endpoint().protocol()); + + { + auto fd (acceptor->native_handle()); + + const int optFalse = 0; + setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&optFalse), sizeof(optFalse)); + + const int optTrue = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&optTrue), sizeof(optTrue)); +#ifndef _WIN32 + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast(&optTrue), sizeof(optTrue)); +#endif /* _WIN32 */ + } + + acceptor->bind(current->endpoint()); + + break; + } catch (const std::exception&) { + if (++current == result.end()) { + throw; + } + + if (acceptor->is_open()) { + acceptor->close(); + } + } + } + } catch (const std::exception& ex) { Log(LogCritical, "ApiListener") - << "Cannot bind TCP socket for host '" << node << "' on port '" << service << "'."; + << "Cannot bind TCP socket for host '" << node << "' on port '" << service << "': " << DiagnosticInformation(ex, false); 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, [this, acceptor, sslContext](asio::yield_context yc) { ListenerCoroutineProc(yc, acceptor, sslContext); }); - m_Servers.insert(server); - - UpdateStatusFile(server); + UpdateStatusFile(localEndpoint); return true; } -void ApiListener::ListenerThreadProc(const Socket::Ptr& server) +void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const std::shared_ptr& server, const std::shared_ptr& sslContext) { - Utility::SetThreadName("API Listener"); + namespace asio = boost::asio; - server->Listen(); + auto& io (server->get_io_service()); for (;;) { try { - Socket::Ptr client = server->Accept(); + auto sslConn (std::make_shared(io, *sslContext)); - /* 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."); + server->async_accept(sslConn->lowest_layer(), yc); + + asio::spawn(io, [this, sslConn](asio::yield_context yc) { NewClientHandler(yc, sslConn, String(), RoleServer); }); + } catch (const std::exception& ex) { + Log(LogCritical, "ApiListener") + << "Cannot accept new connection: " << DiagnosticInformation(ex, false); } } } @@ -383,49 +426,48 @@ void ApiListener::ListenerThreadProc(const Socket::Ptr& server) */ void ApiListener::AddConnection(const Endpoint::Ptr& endpoint) { - { - ObjectLock olock(this); + namespace asio = boost::asio; + using asio::ip::tcp; - std::shared_ptr sslContext = m_SSLContext; + auto sslContext (m_SSLContext); - if (!sslContext) { - Log(LogCritical, "ApiListener", "SSL context is required for AddConnection()"); - return; - } + if (!sslContext) { + Log(LogCritical, "ApiListener", "SSL context is required for AddConnection()"); + return; } - String host = endpoint->GetHost(); - String port = endpoint->GetPort(); + auto& io (IoEngine::Get().GetIoService()); - Log(LogInformation, "ApiListener") - << "Reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'"; + asio::spawn(io, [this, endpoint, &io, sslContext](asio::yield_context yc) { + String host = endpoint->GetHost(); + String port = endpoint->GetPort(); - TcpSocket::Ptr client = new TcpSocket(); - - try { - client->Connect(host, port); - - NewClientHandler(client, endpoint->GetName(), RoleClient); - - endpoint->SetConnecting(false); Log(LogInformation, "ApiListener") - << "Finished reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'"; - } catch (const std::exception& ex) { - endpoint->SetConnecting(false); - client->Close(); + << "Reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'"; - std::ostringstream info; - info << "Cannot connect to host '" << host << "' on port '" << port << "'"; - Log(LogCritical, "ApiListener", info.str()); - Log(LogDebug, "ApiListener") - << info.str() << "\n" << DiagnosticInformation(ex); - } + try { + auto sslConn (std::make_shared(io, *sslContext, endpoint->GetName())); + + Connect(sslConn->lowest_layer(), host, port, yc); + + NewClientHandler(yc, sslConn, endpoint->GetName(), RoleClient); + + endpoint->SetConnecting(false); + Log(LogInformation, "ApiListener") + << "Finished reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'"; + } catch (const std::exception& ex) { + endpoint->SetConnecting(false); + + Log(LogCritical, "ApiListener") + << "Cannot connect to host '" << host << "' on port '" << port << "': " << ex.what(); + } + }); } -void ApiListener::NewClientHandler(const Socket::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(client, hostname, role); + NewClientHandlerInternal(yc, client, hostname, role); } catch (const std::exception& ex) { Log(LogCritical, "ApiListener") << "Exception while handling new API client connection: " << DiagnosticInformation(ex, false); @@ -440,90 +482,90 @@ void ApiListener::NewClientHandler(const Socket::Ptr& client, const String& host * * @param client The new client. */ -void ApiListener::NewClientHandlerInternal(const Socket::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) { - CONTEXT("Handling new API client connection"); + namespace asio = boost::asio; + namespace ssl = asio::ssl; String conninfo; - if (role == RoleClient) - conninfo = "to"; - else - conninfo = "from"; - - conninfo += " " + client->GetPeerAddress(); - - TlsStream::Ptr tlsStream; - - String environmentName = Application::GetAppEnvironment(); - - String serverName = hostname; - - if (!environmentName.IsEmpty()) - serverName += ":" + environmentName; - { - ObjectLock olock(this); - try { - tlsStream = new TlsStream(client, serverName, role, m_SSLContext); - } catch (const std::exception&) { - Log(LogCritical, "ApiListener") - << "Cannot create TLS stream from client connection (" << conninfo << ")"; - return; + 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(); } + auto& sslConn (client->next_layer()); + try { - tlsStream->Handshake(); + 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); - tlsStream->Close(); return; } - std::shared_ptr cert = tlsStream->GetPeerCertificate(); + bool willBeShutDown = false; + + Defer shutDownIfNeeded ([&sslConn, &willBeShutDown, &yc]() { + if (!willBeShutDown) { + sslConn.async_shutdown(yc); + } + }); + + std::shared_ptr cert (SSL_get_peer_certificate(sslConn.native_handle()), X509_free); + bool verify_ok = false; String identity; Endpoint::Ptr endpoint; - bool verify_ok = false; if (cert) { + verify_ok = sslConn.IsVerifyOK(); + + String verifyError = sslConn.GetVerifyError(); + try { identity = GetCertificateCN(cert); } catch (const std::exception&) { Log(LogCritical, "ApiListener") << "Cannot get certificate common name from cert path: '" << GetDefaultCertPath() << "'."; - tlsStream->Close(); return; } - verify_ok = tlsStream->IsVerifyOK(); if (!hostname.IsEmpty()) { if (identity != hostname) { Log(LogWarning, "ApiListener") << "Unexpected certificate common name while connecting to endpoint '" << hostname << "': got '" << identity << "'"; - tlsStream->Close(); return; } else if (!verify_ok) { Log(LogWarning, "ApiListener") << "Certificate validation failed for endpoint '" << hostname - << "': " << tlsStream->GetVerifyError(); + << "': " << verifyError; } } - if (verify_ok) + if (verify_ok) { endpoint = Endpoint::GetByName(identity); + } - { - Log log(LogInformation, "ApiListener"); + Log log(LogInformation, "ApiListener"); - log << "New client connection for identity '" << identity << "' " << conninfo; + log << "New client connection for identity '" << identity << "' " << conninfo; - if (!verify_ok) - log << " (certificate validation failed: " << tlsStream->GetVerifyError() << ")"; - else if (!endpoint) - log << " (no Endpoint object found for identity)"; + if (!verify_ok) { + log << " (certificate validation failed: " << verifyError << ")"; + } else if (!endpoint) { + log << " (no Endpoint object found for identity)"; } } else { Log(LogInformation, "ApiListener") @@ -533,65 +575,84 @@ void ApiListener::NewClientHandlerInternal(const Socket::Ptr& client, const Stri ClientType ctype; if (role == RoleClient) { - Dictionary::Ptr message = new Dictionary({ + JsonRpc::SendMessage(client, new Dictionary({ { "jsonrpc", "2.0" }, { "method", "icinga::Hello" }, { "params", new Dictionary() } - }); + }), yc); + + client->async_flush(yc); - JsonRpc::SendMessage(tlsStream, message); ctype = ClientJsonRpc; } else { - tlsStream->WaitForData(10); + { + boost::system::error_code ec; - if (!tlsStream->IsDataAvailable()) { - 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."; - tlsStream->Close(); - return; + 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; - tlsStream->Peek(&firstByte, 1, false); + char firstByte = 0; - if (firstByte >= '0' && firstByte <= '9') + { + asio::mutable_buffer firstByteBuf (&firstByte, 1); + client->peek(firstByteBuf); + } + + if (firstByte >= '0' && firstByte <= '9') { ctype = ClientJsonRpc; - else + } else { ctype = ClientHttp; + } } if (ctype == ClientJsonRpc) { Log(LogNotice, "ApiListener", "New JSON-RPC client"); - JsonRpcConnection::Ptr aclient = new JsonRpcConnection(identity, verify_ok, tlsStream, role); - aclient->Start(); + JsonRpcConnection::Ptr aclient = new JsonRpcConnection(identity, verify_ok, client, role); if (endpoint) { bool needSync = !endpoint->GetConnected(); endpoint->AddClient(aclient); - m_SyncQueue.Enqueue(std::bind(&ApiListener::SyncClient, this, aclient, endpoint, needSync)); - } else { - if (!AddAnonymousClient(aclient)) { - Log(LogNotice, "ApiListener") - << "Ignoring anonymous JSON-RPC connection " << conninfo - << ". Max connections (" << GetMaxAnonymousClients() << ") exceeded."; - aclient->Disconnect(); - } + asio::spawn(client->get_io_service(), [this, aclient, endpoint, needSync](asio::yield_context yc) { + CpuBoundWork syncClient (yc); + + SyncClient(aclient, endpoint, needSync); + }); + } else if (!AddAnonymousClient(aclient)) { + Log(LogNotice, "ApiListener") + << "Ignoring anonymous JSON-RPC connection " << conninfo + << ". Max connections (" << GetMaxAnonymousClients() << ") exceeded."; + + aclient = nullptr; + } + + if (aclient) { + aclient->Start(); + + willBeShutDown = true; } } else { Log(LogNotice, "ApiListener", "New HTTP client"); - HttpServerConnection::Ptr aclient = new HttpServerConnection(identity, verify_ok, tlsStream); - aclient->Start(); + HttpServerConnection::Ptr aclient = new HttpServerConnection(identity, verify_ok, client); AddHttpClient(aclient); + aclient->Start(); + + willBeShutDown = true; } } @@ -727,10 +788,11 @@ void ApiListener::ApiTimerHandler() } for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) { - if (client->GetTimestamp() != maxTs) - client->Disconnect(); - else + if (client->GetTimestamp() == maxTs) { client->SendMessage(lmessage); + } else { + client->Disconnect(); + } } Log(LogNotice, "ApiListener") @@ -791,8 +853,7 @@ void ApiListener::ApiReconnectTimerHandler() /* Set connecting state to prevent duplicated queue inserts later. */ endpoint->SetConnecting(true); - /* Use dynamic thread pool with additional on demand resources with fast throughput. */ - EnqueueAsyncCallback(std::bind(&ApiListener::AddConnection, this, endpoint), LowLatencyScheduler); + AddConnection(endpoint); } } @@ -1198,8 +1259,7 @@ void ApiListener::ReplayLog(const JsonRpcConnection::Ptr& client) } try { - size_t bytesSent = NetString::WriteStringToStream(client->GetStream(), pmessage->Get("message")); - endpoint->AddMessageSent(bytesSent); + client->SendRawMessage(pmessage->Get("message")); count++; } catch (const std::exception& ex) { Log(LogWarning, "ApiListener") @@ -1224,8 +1284,7 @@ void ApiListener::ReplayLog(const JsonRpcConnection::Ptr& client) }) } }); - size_t bytesSent = JsonRpc::SendMessage(client->GetStream(), lmessage); - endpoint->AddMessageSent(bytesSent); + client->SendMessage(lmessage); } } @@ -1344,8 +1403,6 @@ std::pair ApiListener::GetStatus() /* connection stats */ size_t jsonRpcAnonymousClients = GetAnonymousClients().size(); size_t httpClients = GetHttpClients().size(); - size_t workQueueItems = JsonRpcConnection::GetWorkQueueLength(); - size_t workQueueCount = JsonRpcConnection::GetWorkQueueCount(); size_t syncQueueItems = m_SyncQueue.GetLength(); size_t relayQueueItems = m_RelayQueue.GetLength(); double workQueueItemRate = JsonRpcConnection::GetWorkQueueRate(); @@ -1364,8 +1421,6 @@ std::pair ApiListener::GetStatus() { "json_rpc", new Dictionary({ { "anonymous_clients", jsonRpcAnonymousClients }, - { "work_queue_items", workQueueItems }, - { "work_queue_count", workQueueCount }, { "sync_queue_items", syncQueueItems }, { "relay_queue_items", relayQueueItems }, { "work_queue_item_rate", workQueueItemRate }, @@ -1385,8 +1440,6 @@ std::pair ApiListener::GetStatus() perfdata->Set("num_json_rpc_anonymous_clients", jsonRpcAnonymousClients); perfdata->Set("num_http_clients", httpClients); - perfdata->Set("num_json_rpc_work_queue_items", workQueueItems); - perfdata->Set("num_json_rpc_work_queue_count", workQueueCount); perfdata->Set("num_json_rpc_sync_queue_items", syncQueueItems); perfdata->Set("num_json_rpc_relay_queue_items", relayQueueItems); @@ -1513,14 +1566,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..7072a157f 100644 --- a/lib/remote/apilistener.hpp +++ b/lib/remote/apilistener.hpp @@ -14,6 +14,9 @@ #include "base/tcpsocket.hpp" #include "base/tlsstream.hpp" #include "base/threadpool.hpp" +#include +#include +#include #include namespace icinga @@ -105,8 +108,7 @@ protected: void ValidateTlsHandshakeTimeout(const Lazy& lvalue, const ValidationUtils& utils) override; private: - std::shared_ptr m_SSLContext; - std::set m_Servers; + std::shared_ptr m_SSLContext; mutable boost::mutex m_AnonymousClientsLock; mutable boost::mutex m_HttpClientsLock; @@ -128,12 +130,9 @@ private: bool AddListener(const String& node, const String& service); void AddConnection(const Endpoint::Ptr& endpoint); - 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); + 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); WorkQueue m_RelayQueue; WorkQueue m_SyncQueue{0, 4}; @@ -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 */ diff --git a/lib/remote/configfileshandler.cpp b/lib/remote/configfileshandler.cpp index abd972825..05def8655 100644 --- a/lib/remote/configfileshandler.cpp +++ b/lib/remote/configfileshandler.cpp @@ -12,12 +12,23 @@ using namespace icinga; REGISTER_URLHANDLER("/v1/config/files", ConfigFilesHandler); -bool ConfigFilesHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool ConfigFilesHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestMethod != "GET") + namespace http = boost::beast::http; + + if (request.method() != http::verb::get) return false; - const std::vector& urlPath = request.RequestUrl->GetPath(); + const std::vector& urlPath = url->GetPath(); if (urlPath.size() >= 4) params->Set("package", urlPath[3]); @@ -30,7 +41,7 @@ bool ConfigFilesHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& re params->Set("path", boost::algorithm::join(tmpPath, "/")); } - if (request.Headers->Get("accept") == "application/json") { + if (request[http::field::accept] == "application/json") { HttpUtility::SendJsonError(response, params, 400, "Invalid Accept header. Either remove the Accept header or set it to 'application/octet-stream'."); return true; } @@ -69,9 +80,10 @@ bool ConfigFilesHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& re fp.exceptions(std::ifstream::badbit); String content((std::istreambuf_iterator(fp)), std::istreambuf_iterator()); - response.SetStatus(200, "OK"); - response.AddHeader("Content-Type", "application/octet-stream"); - response.WriteBody(content.CStr(), content.GetLength()); + response.result(http::status::ok); + response.set(http::field::content_type, "application/octet-stream"); + response.body() = content; + response.set(http::field::content_length, response.body().size()); } catch (const std::exception& ex) { HttpUtility::SendJsonError(response, params, 500, "Could not read file.", DiagnosticInformation(ex)); diff --git a/lib/remote/configfileshandler.hpp b/lib/remote/configfileshandler.hpp index 17f03e0dd..1384c2b58 100644 --- a/lib/remote/configfileshandler.hpp +++ b/lib/remote/configfileshandler.hpp @@ -13,8 +13,16 @@ class ConfigFilesHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(ConfigFilesHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/configpackageshandler.cpp b/lib/remote/configpackageshandler.cpp index b590e24fb..2505de36a 100644 --- a/lib/remote/configpackageshandler.cpp +++ b/lib/remote/configpackageshandler.cpp @@ -10,25 +10,44 @@ using namespace icinga; REGISTER_URLHANDLER("/v1/config/packages", ConfigPackagesHandler); -bool ConfigPackagesHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool ConfigPackagesHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() > 4) + namespace http = boost::beast::http; + + if (url->GetPath().size() > 4) return false; - if (request.RequestMethod == "GET") - HandleGet(user, request, response, params); - else if (request.RequestMethod == "POST") - HandlePost(user, request, response, params); - else if (request.RequestMethod == "DELETE") - HandleDelete(user, request, response, params); + if (request.method() == http::verb::get) + HandleGet(user, request, url, response, params); + else if (request.method() == http::verb::post) + HandlePost(user, request, url, response, params); + else if (request.method() == http::verb::delete_) + HandleDelete(user, request, url, response, params); else return false; return true; } -void ConfigPackagesHandler::HandleGet(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +void ConfigPackagesHandler::HandleGet( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params +) { + namespace http = boost::beast::http; + FilterUtility::CheckPermission(user, "config/query"); std::vector packages; @@ -58,16 +77,24 @@ void ConfigPackagesHandler::HandleGet(const ApiUser::Ptr& user, HttpRequest& req { "results", new Array(std::move(results)) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); } -void ConfigPackagesHandler::HandlePost(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +void ConfigPackagesHandler::HandlePost( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params +) { + namespace http = boost::beast::http; + FilterUtility::CheckPermission(user, "config/modify"); - if (request.RequestUrl->GetPath().size() >= 4) - params->Set("package", request.RequestUrl->GetPath()[3]); + if (url->GetPath().size() >= 4) + params->Set("package", url->GetPath()[3]); String packageName = HttpUtility::GetLastParameter(params, "package"); @@ -95,16 +122,24 @@ void ConfigPackagesHandler::HandlePost(const ApiUser::Ptr& user, HttpRequest& re { "results", new Array({ result1 }) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); } -void ConfigPackagesHandler::HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +void ConfigPackagesHandler::HandleDelete( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params +) { + namespace http = boost::beast::http; + FilterUtility::CheckPermission(user, "config/modify"); - if (request.RequestUrl->GetPath().size() >= 4) - params->Set("package", request.RequestUrl->GetPath()[3]); + if (url->GetPath().size() >= 4) + params->Set("package", url->GetPath()[3]); String packageName = HttpUtility::GetLastParameter(params, "package"); @@ -131,6 +166,6 @@ void ConfigPackagesHandler::HandleDelete(const ApiUser::Ptr& user, HttpRequest& { "results", new Array({ result1 }) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); } diff --git a/lib/remote/configpackageshandler.hpp b/lib/remote/configpackageshandler.hpp index 12c081afe..2ae184c6a 100644 --- a/lib/remote/configpackageshandler.hpp +++ b/lib/remote/configpackageshandler.hpp @@ -13,16 +13,39 @@ class ConfigPackagesHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(ConfigPackagesHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; private: - void HandleGet(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params); - void HandlePost(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params); - void HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params); + void HandleGet( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); + void HandlePost( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); + void HandleDelete( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); }; diff --git a/lib/remote/configstageshandler.cpp b/lib/remote/configstageshandler.cpp index fa0078c39..067181f27 100644 --- a/lib/remote/configstageshandler.cpp +++ b/lib/remote/configstageshandler.cpp @@ -11,32 +11,51 @@ using namespace icinga; REGISTER_URLHANDLER("/v1/config/stages", ConfigStagesHandler); -bool ConfigStagesHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool ConfigStagesHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() > 5) + namespace http = boost::beast::http; + + if (url->GetPath().size() > 5) return false; - if (request.RequestMethod == "GET") - HandleGet(user, request, response, params); - else if (request.RequestMethod == "POST") - HandlePost(user, request, response, params); - else if (request.RequestMethod == "DELETE") - HandleDelete(user, request, response, params); + if (request.method() == http::verb::get) + HandleGet(user, request, url, response, params); + else if (request.method() == http::verb::post) + HandlePost(user, request, url, response, params); + else if (request.method() == http::verb::delete_) + HandleDelete(user, request, url, response, params); else return false; return true; } -void ConfigStagesHandler::HandleGet(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +void ConfigStagesHandler::HandleGet( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params +) { + namespace http = boost::beast::http; + FilterUtility::CheckPermission(user, "config/query"); - if (request.RequestUrl->GetPath().size() >= 4) - params->Set("package", request.RequestUrl->GetPath()[3]); + if (url->GetPath().size() >= 4) + params->Set("package", url->GetPath()[3]); - if (request.RequestUrl->GetPath().size() >= 5) - params->Set("stage", request.RequestUrl->GetPath()[4]); + if (url->GetPath().size() >= 5) + params->Set("stage", url->GetPath()[4]); String packageName = HttpUtility::GetLastParameter(params, "package"); String stageName = HttpUtility::GetLastParameter(params, "stage"); @@ -64,16 +83,24 @@ void ConfigStagesHandler::HandleGet(const ApiUser::Ptr& user, HttpRequest& reque { "results", new Array(std::move(results)) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); } -void ConfigStagesHandler::HandlePost(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +void ConfigStagesHandler::HandlePost( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params +) { + namespace http = boost::beast::http; + FilterUtility::CheckPermission(user, "config/modify"); - if (request.RequestUrl->GetPath().size() >= 4) - params->Set("package", request.RequestUrl->GetPath()[3]); + if (url->GetPath().size() >= 4) + params->Set("package", url->GetPath()[3]); String packageName = HttpUtility::GetLastParameter(params, "package"); @@ -123,19 +150,27 @@ void ConfigStagesHandler::HandlePost(const ApiUser::Ptr& user, HttpRequest& requ { "results", new Array({ result1 }) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); } -void ConfigStagesHandler::HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +void ConfigStagesHandler::HandleDelete( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params +) { + namespace http = boost::beast::http; + FilterUtility::CheckPermission(user, "config/modify"); - if (request.RequestUrl->GetPath().size() >= 4) - params->Set("package", request.RequestUrl->GetPath()[3]); + if (url->GetPath().size() >= 4) + params->Set("package", url->GetPath()[3]); - if (request.RequestUrl->GetPath().size() >= 5) - params->Set("stage", request.RequestUrl->GetPath()[4]); + if (url->GetPath().size() >= 5) + params->Set("stage", url->GetPath()[4]); String packageName = HttpUtility::GetLastParameter(params, "package"); String stageName = HttpUtility::GetLastParameter(params, "stage"); @@ -165,7 +200,7 @@ void ConfigStagesHandler::HandleDelete(const ApiUser::Ptr& user, HttpRequest& re { "results", new Array({ result1 }) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); } diff --git a/lib/remote/configstageshandler.hpp b/lib/remote/configstageshandler.hpp index a5ac21605..4ee35ad91 100644 --- a/lib/remote/configstageshandler.hpp +++ b/lib/remote/configstageshandler.hpp @@ -13,16 +13,39 @@ class ConfigStagesHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(ConfigStagesHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; private: - void HandleGet(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params); - void HandlePost(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params); - void HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params); + void HandleGet( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); + void HandlePost( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); + void HandleDelete( + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params + ); }; diff --git a/lib/remote/consolehandler.cpp b/lib/remote/consolehandler.cpp index 67a4988d9..430a37cf3 100644 --- a/lib/remote/consolehandler.cpp +++ b/lib/remote/consolehandler.cpp @@ -53,17 +53,28 @@ static void EnsureFrameCleanupTimer() }); } -bool ConsoleHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool ConsoleHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() != 3) + namespace http = boost::beast::http; + + if (url->GetPath().size() != 3) return false; - if (request.RequestMethod != "POST") + if (request.method() != http::verb::post) return false; QueryDescription qd; - String methodName = request.RequestUrl->GetPath()[2]; + String methodName = url->GetPath()[2]; FilterUtility::CheckPermission(user, "console"); @@ -85,9 +96,12 @@ bool ConsoleHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& reques return true; } -bool ConsoleHandler::ExecuteScriptHelper(HttpRequest& request, HttpResponse& response, +bool ConsoleHandler::ExecuteScriptHelper(boost::beast::http::request& request, + boost::beast::http::response& response, const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed) { + namespace http = boost::beast::http; + Log(LogNotice, "Console") << "Executing expression: " << command; @@ -151,15 +165,18 @@ bool ConsoleHandler::ExecuteScriptHelper(HttpRequest& request, HttpResponse& res { "results", new Array({ resultInfo }) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); return true; } -bool ConsoleHandler::AutocompleteScriptHelper(HttpRequest& request, HttpResponse& response, +bool ConsoleHandler::AutocompleteScriptHelper(boost::beast::http::request& request, + boost::beast::http::response& response, const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed) { + namespace http = boost::beast::http; + Log(LogInformation, "Console") << "Auto-completing expression: " << command; @@ -187,7 +204,7 @@ bool ConsoleHandler::AutocompleteScriptHelper(HttpRequest& request, HttpResponse { "results", new Array({ result1 }) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); return true; diff --git a/lib/remote/consolehandler.hpp b/lib/remote/consolehandler.hpp index 5ef22ca1a..339f3ed86 100644 --- a/lib/remote/consolehandler.hpp +++ b/lib/remote/consolehandler.hpp @@ -22,15 +22,25 @@ class ConsoleHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(ConsoleHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; static std::vector GetAutocompletionSuggestions(const String& word, ScriptFrame& frame); private: - static bool ExecuteScriptHelper(HttpRequest& request, HttpResponse& response, + static bool ExecuteScriptHelper(boost::beast::http::request& request, + boost::beast::http::response& response, const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed); - static bool AutocompleteScriptHelper(HttpRequest& request, HttpResponse& response, + static bool AutocompleteScriptHelper(boost::beast::http::request& request, + boost::beast::http::response& response, const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed); }; diff --git a/lib/remote/createobjecthandler.cpp b/lib/remote/createobjecthandler.cpp index 30eb0bac7..1f7ab6710 100644 --- a/lib/remote/createobjecthandler.cpp +++ b/lib/remote/createobjecthandler.cpp @@ -14,15 +14,26 @@ using namespace icinga; REGISTER_URLHANDLER("/v1/objects", CreateObjectHandler); -bool CreateObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool CreateObjectHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() != 4) + namespace http = boost::beast::http; + + if (url->GetPath().size() != 4) return false; - if (request.RequestMethod != "PUT") + if (request.method() != http::verb::put) return false; - Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); + Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]); if (!type) { HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); @@ -31,7 +42,7 @@ bool CreateObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r FilterUtility::CheckPermission(user, "objects/create/" + type->GetName()); - String name = request.RequestUrl->GetPath()[3]; + String name = url->GetPath()[3]; Array::Ptr templates = params->Get("templates"); Dictionary::Ptr attrs = params->Get("attrs"); @@ -99,7 +110,7 @@ bool CreateObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r result1->Set("code", 500); result1->Set("status", "Object could not be created."); - response.SetStatus(500, "Object could not be created"); + response.result(http::status::internal_server_error); HttpUtility::SendJsonBody(response, params, result); return true; @@ -113,7 +124,7 @@ bool CreateObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r if (verbose) result1->Set("diagnostic_information", diagnosticInformation); - response.SetStatus(500, "Object could not be created"); + response.result(http::status::internal_server_error); HttpUtility::SendJsonBody(response, params, result); return true; @@ -129,7 +140,7 @@ bool CreateObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r else if (!obj && ignoreOnError) result1->Set("status", "Object was not created but 'ignore_on_error' was set to true"); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); return true; diff --git a/lib/remote/createobjecthandler.hpp b/lib/remote/createobjecthandler.hpp index e9da35abf..ff42a62e6 100644 --- a/lib/remote/createobjecthandler.hpp +++ b/lib/remote/createobjecthandler.hpp @@ -13,8 +13,16 @@ class CreateObjectHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(CreateObjectHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/deleteobjecthandler.cpp b/lib/remote/deleteobjecthandler.cpp index 3c2f7c1a8..15951588e 100644 --- a/lib/remote/deleteobjecthandler.cpp +++ b/lib/remote/deleteobjecthandler.cpp @@ -14,15 +14,26 @@ using namespace icinga; REGISTER_URLHANDLER("/v1/objects", DeleteObjectHandler); -bool DeleteObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool DeleteObjectHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() < 3 || request.RequestUrl->GetPath().size() > 4) + namespace http = boost::beast::http; + + if (url->GetPath().size() < 3 || url->GetPath().size() > 4) return false; - if (request.RequestMethod != "DELETE") + if (request.method() != http::verb::delete_) return false; - Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); + Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]); if (!type) { HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); @@ -35,10 +46,10 @@ bool DeleteObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r params->Set("type", type->GetName()); - if (request.RequestUrl->GetPath().size() >= 4) { + if (url->GetPath().size() >= 4) { String attr = type->GetName(); boost::algorithm::to_lower(attr); - params->Set(attr, request.RequestUrl->GetPath()[3]); + params->Set(attr, url->GetPath()[3]); } std::vector objs; @@ -93,9 +104,9 @@ bool DeleteObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r }); if (!success) - response.SetStatus(500, "One or more objects could not be deleted"); + response.result(http::status::internal_server_error); else - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); diff --git a/lib/remote/deleteobjecthandler.hpp b/lib/remote/deleteobjecthandler.hpp index 61cfa8bf7..3ecb0c71b 100644 --- a/lib/remote/deleteobjecthandler.hpp +++ b/lib/remote/deleteobjecthandler.hpp @@ -13,8 +13,16 @@ class DeleteObjectHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(DeleteObjectHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/eventqueue.cpp b/lib/remote/eventqueue.cpp index 20c4af688..6defc2114 100644 --- a/lib/remote/eventqueue.cpp +++ b/lib/remote/eventqueue.cpp @@ -2,8 +2,10 @@ #include "remote/eventqueue.hpp" #include "remote/filterutility.hpp" +#include "base/io-engine.hpp" #include "base/singleton.hpp" #include "base/logger.hpp" +#include using namespace icinga; @@ -100,6 +102,26 @@ Dictionary::Ptr EventQueue::WaitForEvent(void *client, double timeout) } } +Dictionary::Ptr EventQueue::WaitForEvent(void *client, boost::asio::yield_context yc) +{ + for (;;) { + { + boost::mutex::scoped_lock lock(m_Mutex); + + auto it = m_Events.find(client); + ASSERT(it != m_Events.end()); + + if (!it->second.empty()) { + Dictionary::Ptr result = *it->second.begin(); + it->second.pop_front(); + return result; + } + } + + IoBoundWorkSlot dontLockTheIoThreadWhileWaiting (yc); + } +} + std::vector EventQueue::GetQueuesForType(const String& type) { EventQueueRegistry::ItemMap queues = EventQueueRegistry::GetInstance()->GetItems(); diff --git a/lib/remote/eventqueue.hpp b/lib/remote/eventqueue.hpp index 45d23af23..8f6a76c0b 100644 --- a/lib/remote/eventqueue.hpp +++ b/lib/remote/eventqueue.hpp @@ -6,6 +6,7 @@ #include "remote/httphandler.hpp" #include "base/object.hpp" #include "config/expression.hpp" +#include #include #include #include @@ -31,6 +32,7 @@ public: void SetFilter(std::unique_ptr filter); Dictionary::Ptr WaitForEvent(void *client, double timeout = 5); + Dictionary::Ptr WaitForEvent(void *client, boost::asio::yield_context yc); static std::vector GetQueuesForType(const String& type); static void UnregisterIfUnused(const String& name, const EventQueue::Ptr& queue); diff --git a/lib/remote/eventshandler.cpp b/lib/remote/eventshandler.cpp index 7208e3d6b..e6f895cf3 100644 --- a/lib/remote/eventshandler.cpp +++ b/lib/remote/eventshandler.cpp @@ -5,23 +5,39 @@ #include "remote/filterutility.hpp" #include "config/configcompiler.hpp" #include "config/expression.hpp" +#include "base/defer.hpp" +#include "base/io-engine.hpp" #include "base/objectlock.hpp" #include "base/json.hpp" +#include +#include #include using namespace icinga; REGISTER_URLHANDLER("/v1/events", EventsHandler); -bool EventsHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool EventsHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() != 2) + namespace asio = boost::asio; + namespace http = boost::beast::http; + + if (url->GetPath().size() != 2) return false; - if (request.RequestMethod != "POST") + if (request.method() != http::verb::post) return false; - if (request.ProtocolVersion == HttpVersion10) { + if (request.version() == 10) { HttpUtility::SendJsonError(response, params, 400, "HTTP/1.0 not supported for event streams."); return true; } @@ -67,33 +83,37 @@ bool EventsHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request queue->AddClient(&request); - response.SetStatus(200, "OK"); - response.AddHeader("Content-Type", "application/json"); + Defer removeClient ([&queue, &request, &queueName]() { + queue->RemoveClient(&request); + EventQueue::UnregisterIfUnused(queueName, queue); + }); + + hasStartedStreaming = true; + + response.result(http::status::ok); + response.set(http::field::content_type, "application/json"); + + { + IoBoundWorkSlot dontLockTheIoThreadWhileWriting (yc); + + http::async_write(stream, response, yc); + stream.async_flush(yc); + } + + asio::const_buffer newLine ("\n", 1); for (;;) { - Dictionary::Ptr result = queue->WaitForEvent(&request); - - if (!response.IsPeerConnected()) { - queue->RemoveClient(&request); - EventQueue::UnregisterIfUnused(queueName, queue); - return true; - } - - if (!result) - continue; - - String body = JsonEncode(result); + String body = JsonEncode(queue->WaitForEvent(&request, yc)); boost::algorithm::replace_all(body, "\n", ""); - try { - response.WriteBody(body.CStr(), body.GetLength()); - response.WriteBody("\n", 1); - } catch (const std::exception&) { - queue->RemoveClient(&request); - EventQueue::UnregisterIfUnused(queueName, queue); - throw; - } + asio::const_buffer payload (body.CStr(), body.GetLength()); + + IoBoundWorkSlot dontLockTheIoThreadWhileWriting (yc); + + asio::async_write(stream, payload, yc); + asio::async_write(stream, newLine, yc); + stream.async_flush(yc); } } diff --git a/lib/remote/eventshandler.hpp b/lib/remote/eventshandler.hpp index 78f8d03d0..cffeea503 100644 --- a/lib/remote/eventshandler.hpp +++ b/lib/remote/eventshandler.hpp @@ -14,8 +14,16 @@ class EventsHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(EventsHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/httphandler.cpp b/lib/remote/httphandler.cpp index bf8bed37d..c089c00ea 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,20 @@ 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( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + boost::beast::http::response& response, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { 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 +91,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 +99,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(stream, user, request, url, response, params, yc, hasStartedStreaming)) { 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..cec5f58cd 100644 --- a/lib/remote/httphandler.hpp +++ b/lib/remote/httphandler.hpp @@ -4,10 +4,14 @@ #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 "base/tlsstream.hpp" #include +#include +#include namespace icinga { @@ -22,10 +26,26 @@ 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( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) = 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( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + boost::beast::http::response& response, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ); private: static Dictionary::Ptr m_UrlTree; diff --git a/lib/remote/httpserverconnection.cpp b/lib/remote/httpserverconnection.cpp index 8192dfaca..af5169952 100644 --- a/lib/remote/httpserverconnection.cpp +++ b/lib/remote/httpserverconnection.cpp @@ -6,306 +6,144 @@ #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/logger.hpp" #include "base/objectlock.hpp" #include "base/timer.hpp" +#include "base/tlsstream.hpp" #include "base/utility.hpp" +#include +#include +#include +#include +#include +#include +#include #include using namespace icinga; -static boost::once_flag l_HttpServerConnectionOnceFlag = BOOST_ONCE_INIT; -static Timer::Ptr l_HttpServerConnectionTimeoutTimer; +auto const l_ServerHeader ("Icinga/" + Application::GetAppVersion()); -HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream) - : m_Stream(stream), m_Seen(Utility::GetTime()), m_CurrentRequest(stream), m_PendingRequests(0) +HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const std::shared_ptr& stream) + : m_Stream(stream), m_Seen(Utility::GetTime()), m_IoStrand(stream->get_io_service()), m_ShuttingDown(false) { - boost::call_once(l_HttpServerConnectionOnceFlag, &HttpServerConnection::StaticInitialize); - - m_RequestQueue.SetName("HttpServerConnection"); - - if (authenticated) + if (authenticated) { m_ApiUser = ApiUser::GetByClientCN(identity); - - /* Cache the peer address. */ - m_PeerAddress = ""; - - if (stream) { - Socket::Ptr socket = m_Stream->GetSocket(); - - if (socket) { - m_PeerAddress = socket->GetPeerAddress(); - } } -} -void HttpServerConnection::StaticInitialize() -{ - l_HttpServerConnectionTimeoutTimer = new Timer(); - l_HttpServerConnectionTimeoutTimer->OnTimerExpired.connect(std::bind(&HttpServerConnection::TimeoutTimerHandler)); - l_HttpServerConnectionTimeoutTimer->SetInterval(5); - l_HttpServerConnectionTimeoutTimer->Start(); + { + std::ostringstream address; + auto endpoint (stream->lowest_layer().remote_endpoint()); + + address << '[' << endpoint.address() << "]:" << endpoint.port(); + + m_PeerAddress = address.str(); + } } void HttpServerConnection::Start() { - /* the stream holds an owning reference to this object through the callback we're registering here */ - m_Stream->RegisterDataHandler(std::bind(&HttpServerConnection::DataAvailableHandler, HttpServerConnection::Ptr(this))); - if (m_Stream->IsDataAvailable()) - DataAvailableHandler(); -} + namespace asio = boost::asio; -ApiUser::Ptr HttpServerConnection::GetApiUser() const -{ - return m_ApiUser; -} + HttpServerConnection::Ptr keepAlive (this); -TlsStream::Ptr HttpServerConnection::GetStream() const -{ - return m_Stream; + asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { ProcessMessages(yc); }); + asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { CheckLiveness(yc); }); } void HttpServerConnection::Disconnect() { - boost::recursive_mutex::scoped_try_lock lock(m_DataHandlerMutex); - if (!lock.owns_lock()) { - Log(LogInformation, "HttpServerConnection", "Unable to disconnect Http client, I/O thread busy"); - return; - } + namespace asio = boost::asio; - Log(LogInformation, "HttpServerConnection") - << "HTTP client disconnected (from " << m_PeerAddress << ")"; + HttpServerConnection::Ptr keepAlive (this); - ApiListener::Ptr listener = ApiListener::GetInstance(); - listener->RemoveHttpClient(this); + asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { + if (!m_ShuttingDown) { + m_ShuttingDown = true; - m_CurrentRequest.~HttpRequest(); - new (&m_CurrentRequest) HttpRequest(nullptr); + Log(LogInformation, "HttpServerConnection") + << "HTTP client disconnected (from " << m_PeerAddress << ")"; - m_Stream->Close(); -} + try { + m_Stream->next_layer().async_shutdown(yc); + } catch (...) { + } -bool HttpServerConnection::ProcessMessage() -{ - bool res; - HttpResponse response(m_Stream, m_CurrentRequest); + try { + m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both); + } catch (...) { + } - if (!m_CurrentRequest.CompleteHeaders) { - try { - res = m_CurrentRequest.ParseHeaders(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(); + auto listener (ApiListener::GetInstance()); - m_CurrentRequest.~HttpRequest(); - new (&m_CurrentRequest) HttpRequest(m_Stream); + if (listener) { + CpuBoundWork removeHttpClient (yc); - 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); + listener->RemoveHttpClient(this); } } + }); +} - response.AddHeader("Access-Control-Allow-Credentials", "true"); +static inline +bool EnsureValidHeaders( + AsioTlsStream& stream, + boost::beast::flat_buffer& buf, + boost::beast::http::parser& parser, + boost::beast::http::response& response, + boost::asio::yield_context& yc +) +{ + namespace http = boost::beast::http; - String accessControlRequestMethodHeader = m_CurrentRequest.Headers->Get("access-control-request-method"); + bool httpError = true; - 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; + try { + try { + 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()); } - } - 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; - } + httpError = false; - if (!m_AuthenticatedUser) { - Log(LogWarning, "HttpServerConnection") - << "Unauthorized request: " << m_CurrentRequest.RequestMethod << " " << requestUrl; + 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.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); + if (!httpError && parser.get()[http::field::accept] == "application/json") { + HttpUtility::SendJsonBody(response, nullptr, new Dictionary({ + { "error", 400 }, + { "status", String("Bad Request: ") + ex.what() } + })); } else { - response.AddHeader("Content-Type", "text/html"); - String msg = "

Unauthorized. Please check your user credentials.

"; - response.WriteBody(msg.CStr(), msg.GetLength()); + 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.Finish(); - return false; - } + response.set(http::field::connection, "close"); - 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 +151,370 @@ 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); + + if (parser.get()[http::field::accept] == "application/json") { + HttpUtility::SendJsonBody(response, nullptr, new Dictionary({ + { "error", 400 }, + { "status", String("Bad Request: ") + ex.what() } + })); + } else { + 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 +bool 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; + + bool hasStartedStreaming = false; + + try { + CpuBoundWork handlingRequest (yc); + + HttpHandler::ProcessRequest(stream, authenticatedUser, request, response, yc, hasStartedStreaming); } 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; + if (hasStartedStreaming) { + return false; } - try { - while (ProcessMessage()) - ; /* empty loop body */ - } catch (const std::exception& ex) { - Log(LogWarning, "HttpServerConnection") - << "Error while reading Http request: " << DiagnosticInformation(ex); + http::response response; - close = true; + 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; + + Defer disconnect ([this]() { Disconnect(); }); + + try { + beast::flat_buffer buf; + + for (;;) { + m_Seen = Utility::GetTime(); + + http::parser parser; + http::response response; + + parser.header_limit(1024 * 1024); + parser.body_limit(-1); + + response.set(http::field::server, l_ServerHeader); + + if (!EnsureValidHeaders(*m_Stream, buf, parser, response, yc)) { + break; + } + + m_Seen = Utility::GetTime(); + + 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; + } + + m_Seen = std::numeric_limits::max(); + + if (!ProcessRequest(*m_Stream, request, authenticatedUser, response, yc)) { + break; + } + + if (request.version() != 11 || request[http::field::connection] == "close") { + break; + } + } + } catch (const std::exception& ex) { + if (!m_ShuttingDown) { + Log(LogCritical, "HttpServerConnection") + << "Unhandled exception while processing HTTP request: " << DiagnosticInformation(ex); } - } 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(); } } -void HttpServerConnection::TimeoutTimerHandler() +void HttpServerConnection::CheckLiveness(boost::asio::yield_context yc) { - ApiListener::Ptr listener = ApiListener::GetInstance(); + boost::asio::deadline_timer timer (m_Stream->get_io_service()); - for (const HttpServerConnection::Ptr& client : listener->GetHttpClients()) { - client->CheckLiveness(); + for (;;) { + timer.expires_from_now(boost::posix_time::seconds(5)); + timer.async_wait(yc); + + 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; + } } } - diff --git a/lib/remote/httpserverconnection.hpp b/lib/remote/httpserverconnection.hpp index e27ba839c..7db97ed68 100644 --- a/lib/remote/httpserverconnection.hpp +++ b/lib/remote/httpserverconnection.hpp @@ -3,12 +3,12 @@ #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 +#include namespace icinga { @@ -23,39 +23,21 @@ 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; + std::shared_ptr m_Stream; double m_Seen; - HttpRequest m_CurrentRequest; - boost::recursive_mutex m_DataHandlerMutex; - WorkQueue m_RequestQueue; - int m_PendingRequests; String m_PeerAddress; + boost::asio::io_service::strand m_IoStrand; + bool m_ShuttingDown; - 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); + void CheckLiveness(boost::asio::yield_context yc); }; } diff --git a/lib/remote/httputility.cpp b/lib/remote/httputility.cpp index 043f3cf9e..c97297cd0 100644 --- a/lib/remote/httputility.cpp +++ b/lib/remote/httputility.cpp @@ -1,27 +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 +#include #include +#include using namespace icinga; -Dictionary::Ptr HttpUtility::FetchRequestParameters(HttpRequest& request) +Dictionary::Ptr HttpUtility::FetchRequestParameters(const Url::Ptr& url, const std::string& body) { Dictionary::Ptr result; - String body; - char buffer[1024]; - size_t count; - - while ((count = request.ReadBody(buffer, sizeof(buffer))) > 0) - body += String(buffer, buffer + count); - - if (!body.IsEmpty()) { + if (!body.empty()) { Log(LogDebug, "HttpUtility") - << "Request body: '" << body << "'"; + << "Request body: '" << body << '\''; result = JsonDecode(body); } @@ -30,7 +26,7 @@ Dictionary::Ptr HttpUtility::FetchRequestParameters(HttpRequest& request) result = new Dictionary(); std::map> query; - for (const auto& kv : request.RequestUrl->GetQuery()) { + for (const auto& kv : url->GetQuery()) { query[kv.first].emplace_back(kv.second); } @@ -55,6 +51,15 @@ void HttpUtility::SendJsonBody(HttpResponse& response, const Dictionary::Ptr& pa response.WriteBody(body.CStr(), body.GetLength()); } +void HttpUtility::SendJsonBody(boost::beast::http::response& 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.set(http::field::content_length, response.body().size()); +} + Value HttpUtility::GetLastParameter(const Dictionary::Ptr& params, const String& key) { Value varr = params->Get(key); @@ -93,6 +98,24 @@ void HttpUtility::SendJsonError(HttpResponse& response, const Dictionary::Ptr& p HttpUtility::SendJsonBody(response, params, result); } +void HttpUtility::SendJsonError(boost::beast::http::response& response, + const Dictionary::Ptr& params, int code, const String& info, const String& diagnosticInformation) +{ + Dictionary::Ptr result = new Dictionary({ { "error", code } }); + + if (!info.IsEmpty()) { + result->Set("status", info); + } + + if (params && HttpUtility::GetLastParameter(params, "verbose") && !diagnosticInformation.IsEmpty()) { + result->Set("diagnostic_information", diagnosticInformation); + } + + response.result(code); + + HttpUtility::SendJsonBody(response, params, result); +} + String HttpUtility::GetErrorNameByCode(const int code) { switch(code) { diff --git a/lib/remote/httputility.hpp b/lib/remote/httputility.hpp index 0a0418161..e9f44b3aa 100644 --- a/lib/remote/httputility.hpp +++ b/lib/remote/httputility.hpp @@ -5,7 +5,10 @@ #include "remote/httprequest.hpp" #include "remote/httpresponse.hpp" +#include "remote/url.hpp" #include "base/dictionary.hpp" +#include +#include namespace icinga { @@ -19,11 +22,14 @@ class HttpUtility { public: - static Dictionary::Ptr FetchRequestParameters(HttpRequest& request); + static Dictionary::Ptr FetchRequestParameters(const Url::Ptr& url, const std::string& body); static void SendJsonBody(HttpResponse& response, const Dictionary::Ptr& params, const Value& val); + static void SendJsonBody(boost::beast::http::response& response, const Dictionary::Ptr& params, const Value& val); static Value GetLastParameter(const Dictionary::Ptr& params, const String& key); static void SendJsonError(HttpResponse& response, const Dictionary::Ptr& params, const int code, const String& verbose = String(), const String& diagnosticInformation = String()); + static void SendJsonError(boost::beast::http::response& response, const Dictionary::Ptr& params, const int code, + const String& verbose = String(), const String& diagnosticInformation = String()); private: static String GetErrorNameByCode(int code); diff --git a/lib/remote/infohandler.cpp b/lib/remote/infohandler.cpp index 6925a668f..38f99781d 100644 --- a/lib/remote/infohandler.cpp +++ b/lib/remote/infohandler.cpp @@ -8,24 +8,35 @@ using namespace icinga; REGISTER_URLHANDLER("/", InfoHandler); -bool InfoHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool InfoHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() > 2) + namespace http = boost::beast::http; + + if (url->GetPath().size() > 2) return false; - if (request.RequestMethod != "GET") + if (request.method() != http::verb::get) return false; - if (request.RequestUrl->GetPath().empty()) { - response.SetStatus(302, "Found"); - response.AddHeader("Location", "/v1"); + if (url->GetPath().empty()) { + response.result(http::status::found); + response.set(http::field::location, "/v1"); return true; } - if (request.RequestUrl->GetPath()[0] != "v1" || request.RequestUrl->GetPath().size() != 1) + if (url->GetPath()[0] != "v1" || url->GetPath().size() != 1) return false; - response.SetStatus(200, "OK"); + response.result(http::status::ok); std::vector permInfo; Array::Ptr permissions = user->GetPermissions(); @@ -49,7 +60,7 @@ bool InfoHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, } } - if (request.Headers->Get("accept") == "application/json") { + if (request[http::field::accept] == "application/json") { Dictionary::Ptr result1 = new Dictionary({ { "user", user->GetName() }, { "permissions", Array::FromVector(permInfo) }, @@ -63,7 +74,7 @@ bool InfoHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpUtility::SendJsonBody(response, params, result); } else { - response.AddHeader("Content-Type", "text/html"); + response.set(http::field::content_type, "text/html"); String body = "Icinga 2

Hello from Icinga 2 (Version: " + Application::GetAppVersion() + ")!

"; body += "

You are authenticated as " + user->GetName() + ". "; @@ -80,7 +91,8 @@ bool InfoHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, body += "Your user does not have any permissions.

"; body += R"(

More information about API requests is available in the documentation.

)"; - response.WriteBody(body.CStr(), body.GetLength()); + response.body() = body; + response.set(http::field::content_length, response.body().size()); } return true; diff --git a/lib/remote/infohandler.hpp b/lib/remote/infohandler.hpp index 38c88af11..346340d81 100644 --- a/lib/remote/infohandler.hpp +++ b/lib/remote/infohandler.hpp @@ -13,8 +13,16 @@ class InfoHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(InfoHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/jsonrpc.cpp b/lib/remote/jsonrpc.cpp index 1c429ea7c..03f3c7d0e 100644 --- a/lib/remote/jsonrpc.cpp +++ b/lib/remote/jsonrpc.cpp @@ -6,7 +6,11 @@ #include "base/console.hpp" #include "base/scriptglobal.hpp" #include "base/convert.hpp" +#include "base/tlsstream.hpp" #include +#include +#include +#include using namespace icinga; @@ -55,6 +59,35 @@ size_t JsonRpc::SendMessage(const Stream::Ptr& stream, const Dictionary::Ptr& me return NetString::WriteStringToStream(stream, json); } +/** + * Sends a message to the connected peer and returns the bytes sent. + * + * @param message The message. + * + * @return The amount of bytes sent. + */ +size_t JsonRpc::SendMessage(const std::shared_ptr& stream, const Dictionary::Ptr& message, boost::asio::yield_context yc) +{ + return JsonRpc::SendRawMessage(stream, JsonEncode(message), yc); +} + +/** + * Sends a message to the connected peer and returns the bytes sent. + * + * @param message The message. + * + * @return The amount of bytes sent. + */ +size_t JsonRpc::SendRawMessage(const std::shared_ptr& stream, const String& json, boost::asio::yield_context yc) +{ +#ifdef I2_DEBUG + if (GetDebugJsonRpcCached()) + std::cerr << ConsoleColorTag(Console_ForegroundBlue) << ">> " << json << ConsoleColorTag(Console_Normal) << "\n"; +#endif /* I2_DEBUG */ + + return NetString::WriteStringToStream(stream, json, yc); +} + StreamReadStatus JsonRpc::ReadMessage(const Stream::Ptr& stream, String *message, StreamReadContext& src, bool may_wait, ssize_t maxMessageLength) { String jsonString; @@ -73,6 +106,18 @@ StreamReadStatus JsonRpc::ReadMessage(const Stream::Ptr& stream, String *message return StatusNewItem; } +String JsonRpc::ReadMessage(const std::shared_ptr& stream, boost::asio::yield_context yc, ssize_t maxMessageLength) +{ + String jsonString = NetString::ReadStringFromStream(stream, yc, maxMessageLength); + +#ifdef I2_DEBUG + if (GetDebugJsonRpcCached()) + std::cerr << ConsoleColorTag(Console_ForegroundBlue) << "<< " << jsonString << ConsoleColorTag(Console_Normal) << "\n"; +#endif /* I2_DEBUG */ + + return std::move(jsonString); +} + Dictionary::Ptr JsonRpc::DecodeMessage(const String& message) { Value value = JsonDecode(message); diff --git a/lib/remote/jsonrpc.hpp b/lib/remote/jsonrpc.hpp index 44e8a9360..faf9c07e8 100644 --- a/lib/remote/jsonrpc.hpp +++ b/lib/remote/jsonrpc.hpp @@ -5,7 +5,10 @@ #include "base/stream.hpp" #include "base/dictionary.hpp" +#include "base/tlsstream.hpp" #include "remote/i2-remote.hpp" +#include +#include namespace icinga { @@ -19,7 +22,10 @@ class JsonRpc { public: static size_t SendMessage(const Stream::Ptr& stream, const Dictionary::Ptr& message); + static size_t SendMessage(const std::shared_ptr& stream, const Dictionary::Ptr& message, boost::asio::yield_context yc); + static size_t SendRawMessage(const std::shared_ptr& stream, const String& json, boost::asio::yield_context yc); static StreamReadStatus ReadMessage(const Stream::Ptr& stream, String *message, StreamReadContext& src, bool may_wait = false, ssize_t maxMessageLength = -1); + static String ReadMessage(const std::shared_ptr& stream, boost::asio::yield_context yc, ssize_t maxMessageLength = -1); static Dictionary::Ptr DecodeMessage(const String& message); private: diff --git a/lib/remote/jsonrpcconnection-heartbeat.cpp b/lib/remote/jsonrpcconnection-heartbeat.cpp index 5b466830c..f2e7f4045 100644 --- a/lib/remote/jsonrpcconnection-heartbeat.cpp +++ b/lib/remote/jsonrpcconnection-heartbeat.cpp @@ -7,34 +7,42 @@ #include "base/configtype.hpp" #include "base/logger.hpp" #include "base/utility.hpp" +#include +#include +#include using namespace icinga; REGISTER_APIFUNCTION(Heartbeat, event, &JsonRpcConnection::HeartbeatAPIHandler); -void JsonRpcConnection::HeartbeatTimerHandler() +void JsonRpcConnection::HandleAndWriteHeartbeats(boost::asio::yield_context yc) { - for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType()) { - for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) { - if (client->m_NextHeartbeat != 0 && client->m_NextHeartbeat < Utility::GetTime()) { - Log(LogWarning, "JsonRpcConnection") - << "Client for endpoint '" << endpoint->GetName() << "' has requested " - << "heartbeat message but hasn't responded in time. Closing connection."; + boost::asio::deadline_timer timer (m_Stream->get_io_service()); - client->Disconnect(); - continue; - } + for (;;) { + timer.expires_from_now(boost::posix_time::seconds(10)); + timer.async_wait(yc); - Dictionary::Ptr request = new Dictionary({ - { "jsonrpc", "2.0" }, - { "method", "event::Heartbeat" }, - { "params", new Dictionary({ - { "timeout", 120 } - }) } - }); - - client->SendMessage(request); + if (m_ShuttingDown) { + break; } + + if (m_NextHeartbeat != 0 && m_NextHeartbeat < Utility::GetTime()) { + Log(LogWarning, "JsonRpcConnection") + << "Client for endpoint '" << m_Endpoint->GetName() << "' has requested " + << "heartbeat message but hasn't responded in time. Closing connection."; + + Disconnect(); + break; + } + + SendMessageInternal(new Dictionary({ + { "jsonrpc", "2.0" }, + { "method", "event::Heartbeat" }, + { "params", new Dictionary({ + { "timeout", 120 } + }) } + })); } } @@ -44,7 +52,6 @@ Value JsonRpcConnection::HeartbeatAPIHandler(const MessageOrigin::Ptr& origin, c if (!vtimeout.IsEmpty()) { origin->FromClient->m_NextHeartbeat = Utility::GetTime() + vtimeout; - origin->FromClient->m_HeartbeatTimeout = vtimeout; } return Empty; diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp index 8eb82ed40..66f88479b 100644 --- a/lib/remote/jsonrpcconnection-pki.cpp +++ b/lib/remote/jsonrpcconnection-pki.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include using namespace icinga; @@ -30,10 +32,12 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona Dictionary::Ptr result = new Dictionary(); /* Use the presented client certificate if not provided. */ - if (certText.IsEmpty()) - cert = origin->FromClient->GetStream()->GetPeerCertificate(); - else + if (certText.IsEmpty()) { + auto stream (origin->FromClient->GetStream()); + cert = std::shared_ptr(SSL_get_peer_certificate(stream->next_layer().native_handle()), X509_free); + } else { cert = StringToCertificate(certText); + } if (!cert) { Log(LogWarning, "JsonRpcConnection") << "No certificate or CSR received"; @@ -121,7 +125,7 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona { "method", "pki::UpdateCertificate" }, { "params", result } }); - JsonRpc::SendMessage(client->GetStream(), message); + client->SendMessage(message); return result; } @@ -192,7 +196,7 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona { "method", "pki::UpdateCertificate" }, { "params", result } }); - JsonRpc::SendMessage(client->GetStream(), message); + client->SendMessage(message); return result; @@ -255,7 +259,7 @@ void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& acl * or b) the local zone and all parents. */ if (aclient) - JsonRpc::SendMessage(aclient->GetStream(), message); + aclient->SendMessage(message); else listener->RelayMessage(origin, Zone::GetLocalZone(), message, false); } diff --git a/lib/remote/jsonrpcconnection.cpp b/lib/remote/jsonrpcconnection.cpp index 99d0d7feb..b850b7884 100644 --- a/lib/remote/jsonrpcconnection.cpp +++ b/lib/remote/jsonrpcconnection.cpp @@ -4,12 +4,21 @@ #include "remote/apilistener.hpp" #include "remote/apifunction.hpp" #include "remote/jsonrpc.hpp" +#include "base/defer.hpp" #include "base/configtype.hpp" +#include "base/io-engine.hpp" +#include "base/json.hpp" #include "base/objectlock.hpp" #include "base/utility.hpp" #include "base/logger.hpp" #include "base/exception.hpp" #include "base/convert.hpp" +#include "base/tlsstream.hpp" +#include +#include +#include +#include +#include #include using namespace icinga; @@ -17,50 +26,108 @@ using namespace icinga; static Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params); REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler); -static boost::once_flag l_JsonRpcConnectionOnceFlag = BOOST_ONCE_INIT; -static Timer::Ptr l_JsonRpcConnectionTimeoutTimer; -static WorkQueue *l_JsonRpcConnectionWorkQueues; -static size_t l_JsonRpcConnectionWorkQueueCount; -static int l_JsonRpcConnectionNextID; -static Timer::Ptr l_HeartbeatTimer; +static RingBuffer l_TaskStats (15 * 60); JsonRpcConnection::JsonRpcConnection(const String& identity, bool authenticated, - TlsStream::Ptr stream, ConnectionRole role) - : m_ID(l_JsonRpcConnectionNextID++), m_Identity(identity), m_Authenticated(authenticated), m_Stream(std::move(stream)), - m_Role(role), m_Timestamp(Utility::GetTime()), m_Seen(Utility::GetTime()), m_NextHeartbeat(0), m_HeartbeatTimeout(0) + const std::shared_ptr& stream, ConnectionRole role) + : m_Identity(identity), m_Authenticated(authenticated), m_Stream(stream), + m_Role(role), m_Timestamp(Utility::GetTime()), m_Seen(Utility::GetTime()), m_NextHeartbeat(0), m_IoStrand(stream->get_io_service()), + m_OutgoingMessagesQueued(stream->get_io_service()), m_WriterDone(stream->get_io_service()), m_ShuttingDown(false) { - boost::call_once(l_JsonRpcConnectionOnceFlag, &JsonRpcConnection::StaticInitialize); - if (authenticated) m_Endpoint = Endpoint::GetByName(identity); } -void JsonRpcConnection::StaticInitialize() -{ - l_JsonRpcConnectionTimeoutTimer = new Timer(); - l_JsonRpcConnectionTimeoutTimer->OnTimerExpired.connect(std::bind(&JsonRpcConnection::TimeoutTimerHandler)); - l_JsonRpcConnectionTimeoutTimer->SetInterval(15); - l_JsonRpcConnectionTimeoutTimer->Start(); - - l_JsonRpcConnectionWorkQueueCount = Configuration::Concurrency; - l_JsonRpcConnectionWorkQueues = new WorkQueue[l_JsonRpcConnectionWorkQueueCount]; - - for (size_t i = 0; i < l_JsonRpcConnectionWorkQueueCount; i++) { - l_JsonRpcConnectionWorkQueues[i].SetName("JsonRpcConnection, #" + Convert::ToString(i)); - } - - l_HeartbeatTimer = new Timer(); - l_HeartbeatTimer->OnTimerExpired.connect(std::bind(&JsonRpcConnection::HeartbeatTimerHandler)); - l_HeartbeatTimer->SetInterval(10); - l_HeartbeatTimer->Start(); -} - void JsonRpcConnection::Start() { - /* the stream holds an owning reference to this object through the callback we're registering here */ - m_Stream->RegisterDataHandler(std::bind(&JsonRpcConnection::DataAvailableHandler, JsonRpcConnection::Ptr(this))); - if (m_Stream->IsDataAvailable()) - DataAvailableHandler(); + namespace asio = boost::asio; + + JsonRpcConnection::Ptr keepAlive (this); + + asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { HandleIncomingMessages(yc); }); + asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { WriteOutgoingMessages(yc); }); + asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { HandleAndWriteHeartbeats(yc); }); + asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { CheckLiveness(yc); }); +} + +void JsonRpcConnection::HandleIncomingMessages(boost::asio::yield_context yc) +{ + Defer disconnect ([this]() { Disconnect(); }); + + for (;;) { + String message; + + try { + message = JsonRpc::ReadMessage(m_Stream, yc, m_Endpoint ? -1 : 1024 * 1024); + } catch (const std::exception& ex) { + if (!m_ShuttingDown) { + Log(LogWarning, "JsonRpcConnection") + << "Error while reading JSON-RPC message for identity '" << m_Identity + << "': " << DiagnosticInformation(ex); + } + + break; + } + + m_Seen = Utility::GetTime(); + + try { + CpuBoundWork handleMessage (yc); + + MessageHandler(message); + } catch (const std::exception& ex) { + if (!m_ShuttingDown) { + Log(LogWarning, "JsonRpcConnection") + << "Error while processing JSON-RPC message for identity '" << m_Identity + << "': " << DiagnosticInformation(ex); + } + + break; + } + + CpuBoundWork taskStats (yc); + + l_TaskStats.InsertValue(Utility::GetTime(), 1); + } +} + +void JsonRpcConnection::WriteOutgoingMessages(boost::asio::yield_context yc) +{ + Defer disconnect ([this]() { Disconnect(); }); + + Defer signalWriterDone ([this]() { m_WriterDone.Set(); }); + + do { + m_OutgoingMessagesQueued.Wait(yc); + + auto queue (std::move(m_OutgoingMessagesQueue)); + + m_OutgoingMessagesQueue.clear(); + m_OutgoingMessagesQueued.Clear(); + + if (!queue.empty()) { + try { + for (auto& message : queue) { + size_t bytesSent = JsonRpc::SendRawMessage(m_Stream, message, yc); + + if (m_Endpoint) { + m_Endpoint->AddMessageSent(bytesSent); + } + } + + m_Stream->async_flush(yc); + } catch (const std::exception& ex) { + if (!m_ShuttingDown) { + std::ostringstream info; + info << "Error while sending JSON-RPC message for identity '" << m_Identity << "'"; + Log(LogWarning, "JsonRpcConnection") + << info.str() << "\n" << DiagnosticInformation(ex); + } + + break; + } + } + } while (!m_ShuttingDown); } double JsonRpcConnection::GetTimestamp() const @@ -83,7 +150,7 @@ Endpoint::Ptr JsonRpcConnection::GetEndpoint() const return m_Endpoint; } -TlsStream::Ptr JsonRpcConnection::GetStream() const +std::shared_ptr JsonRpcConnection::GetStream() const { return m_Stream; } @@ -95,69 +162,66 @@ ConnectionRole JsonRpcConnection::GetRole() const void JsonRpcConnection::SendMessage(const Dictionary::Ptr& message) { - try { - ObjectLock olock(m_Stream); + m_IoStrand.post([this, message]() { SendMessageInternal(message); }); +} - if (m_Stream->IsEof()) - return; +void JsonRpcConnection::SendRawMessage(const String& message) +{ + m_IoStrand.post([this, message]() { + m_OutgoingMessagesQueue.emplace_back(message); + m_OutgoingMessagesQueued.Set(); + }); +} - size_t bytesSent = JsonRpc::SendMessage(m_Stream, message); - - if (m_Endpoint) - m_Endpoint->AddMessageSent(bytesSent); - - } catch (const std::exception& ex) { - std::ostringstream info; - info << "Error while sending JSON-RPC message for identity '" << m_Identity << "'"; - Log(LogWarning, "JsonRpcConnection") - << info.str() << "\n" << DiagnosticInformation(ex); - - Disconnect(); - } +void JsonRpcConnection::SendMessageInternal(const Dictionary::Ptr& message) +{ + m_OutgoingMessagesQueue.emplace_back(JsonEncode(message)); + m_OutgoingMessagesQueued.Set(); } void JsonRpcConnection::Disconnect() { - Log(LogWarning, "JsonRpcConnection") - << "API client disconnected for identity '" << m_Identity << "'"; + namespace asio = boost::asio; - m_Stream->Close(); + JsonRpcConnection::Ptr keepAlive (this); - if (m_Endpoint) - m_Endpoint->RemoveClient(this); - else { - ApiListener::Ptr listener = ApiListener::GetInstance(); - listener->RemoveAnonymousClient(this); - } -} + asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { + if (!m_ShuttingDown) { + m_ShuttingDown = true; -void JsonRpcConnection::MessageHandlerWrapper(const String& jsonString) -{ - if (m_Stream->IsEof()) - return; + Log(LogWarning, "JsonRpcConnection") + << "API client disconnected for identity '" << m_Identity << "'"; - try { - MessageHandler(jsonString); - } catch (const std::exception& ex) { - Log(LogWarning, "JsonRpcConnection") - << "Error while reading JSON-RPC message for identity '" << m_Identity - << "': " << DiagnosticInformation(ex); + m_OutgoingMessagesQueued.Set(); - Disconnect(); + m_WriterDone.Wait(yc); - return; - } + try { + m_Stream->next_layer().async_shutdown(yc); + } catch (...) { + } + + try { + m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both); + } catch (...) { + } + + CpuBoundWork removeClient (yc); + + if (m_Endpoint) { + m_Endpoint->RemoveClient(this); + } else { + auto listener (ApiListener::GetInstance()); + listener->RemoveAnonymousClient(this); + } + } + }); } void JsonRpcConnection::MessageHandler(const String& jsonString) { Dictionary::Ptr message = JsonRpc::DecodeMessage(jsonString); - m_Seen = Utility::GetTime(); - - if (m_HeartbeatTimeout != 0) - m_NextHeartbeat = Utility::GetTime() + m_HeartbeatTimeout; - if (m_Endpoint && message->Contains("ts")) { double ts = message->Get("ts"); @@ -225,59 +289,11 @@ void JsonRpcConnection::MessageHandler(const String& jsonString) if (message->Contains("id")) { resultMessage->Set("jsonrpc", "2.0"); resultMessage->Set("id", message->Get("id")); - SendMessage(resultMessage); + + SendMessageInternal(resultMessage); } } -bool JsonRpcConnection::ProcessMessage() -{ - /* Limit for anonymous clients (signing requests and not configured endpoints. */ - ssize_t maxMessageLength = 1024 * 1024; - - if (m_Endpoint) - maxMessageLength = -1; /* no limit */ - - String message; - - StreamReadStatus srs = JsonRpc::ReadMessage(m_Stream, &message, m_Context, false, maxMessageLength); - - if (srs != StatusNewItem) - return false; - - l_JsonRpcConnectionWorkQueues[m_ID % l_JsonRpcConnectionWorkQueueCount].Enqueue(std::bind(&JsonRpcConnection::MessageHandlerWrapper, JsonRpcConnection::Ptr(this), message)); - - return true; -} - -void JsonRpcConnection::DataAvailableHandler() -{ - bool close = false; - - if (!m_Stream) - return; - - if (!m_Stream->IsEof()) { - boost::mutex::scoped_lock lock(m_DataHandlerMutex); - - try { - while (ProcessMessage()) - ; /* empty loop body */ - } catch (const std::exception& ex) { - Log(LogWarning, "JsonRpcConnection") - << "Error while reading JSON-RPC message for identity '" << m_Identity - << "': " << DiagnosticInformation(ex); - - Disconnect(); - - return; - } - } else - close = true; - - if (close) - Disconnect(); -} - Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params) { double log_position = params->Get("log_position"); @@ -292,57 +308,29 @@ Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary:: return Empty; } -void JsonRpcConnection::CheckLiveness() +void JsonRpcConnection::CheckLiveness(boost::asio::yield_context yc) { - if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) { - Log(LogInformation, "JsonRpcConnection") - << "No messages for identity '" << m_Identity << "' have been received in the last 60 seconds."; - Disconnect(); - } -} + boost::asio::deadline_timer timer (m_Stream->get_io_service()); -void JsonRpcConnection::TimeoutTimerHandler() -{ - ApiListener::Ptr listener = ApiListener::GetInstance(); + for (;;) { + timer.expires_from_now(boost::posix_time::seconds(30)); + timer.async_wait(yc); - for (const JsonRpcConnection::Ptr& client : listener->GetAnonymousClients()) { - client->CheckLiveness(); - } + if (m_ShuttingDown) { + break; + } - for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType()) { - for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) { - client->CheckLiveness(); + if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) { + Log(LogInformation, "JsonRpcConnection") + << "No messages for identity '" << m_Identity << "' have been received in the last 60 seconds."; + + Disconnect(); + break; } } } -size_t JsonRpcConnection::GetWorkQueueCount() -{ - return l_JsonRpcConnectionWorkQueueCount; -} - -size_t JsonRpcConnection::GetWorkQueueLength() -{ - size_t itemCount = 0; - - for (size_t i = 0; i < GetWorkQueueCount(); i++) - itemCount += l_JsonRpcConnectionWorkQueues[i].GetLength(); - - return itemCount; -} - double JsonRpcConnection::GetWorkQueueRate() { - double rate = 0.0; - size_t count = GetWorkQueueCount(); - - /* If this is a standalone environment, we don't have any queues. */ - if (count == 0) - return 0.0; - - for (size_t i = 0; i < count; i++) - rate += l_JsonRpcConnectionWorkQueues[i].GetTaskCount(60) / 60.0; - - return rate / count; + return l_TaskStats.UpdateAndGetValues(Utility::GetTime(), 60) / 60.0; } - diff --git a/lib/remote/jsonrpcconnection.hpp b/lib/remote/jsonrpcconnection.hpp index 40df9af13..994dd7368 100644 --- a/lib/remote/jsonrpcconnection.hpp +++ b/lib/remote/jsonrpcconnection.hpp @@ -5,9 +5,14 @@ #include "remote/i2-remote.hpp" #include "remote/endpoint.hpp" +#include "base/io-engine.hpp" #include "base/tlsstream.hpp" #include "base/timer.hpp" #include "base/workqueue.hpp" +#include +#include +#include +#include namespace icinga { @@ -36,7 +41,7 @@ class JsonRpcConnection final : public Object public: DECLARE_PTR_TYPEDEFS(JsonRpcConnection); - JsonRpcConnection(const String& identity, bool authenticated, TlsStream::Ptr stream, ConnectionRole role); + JsonRpcConnection(const String& identity, bool authenticated, const std::shared_ptr& stream, ConnectionRole role); void Start(); @@ -44,47 +49,46 @@ public: String GetIdentity() const; bool IsAuthenticated() const; Endpoint::Ptr GetEndpoint() const; - TlsStream::Ptr GetStream() const; + std::shared_ptr GetStream() const; ConnectionRole GetRole() const; void Disconnect(); void SendMessage(const Dictionary::Ptr& request); + void SendRawMessage(const String& request); - static void HeartbeatTimerHandler(); static Value HeartbeatAPIHandler(const intrusive_ptr& origin, const Dictionary::Ptr& params); - static size_t GetWorkQueueCount(); - static size_t GetWorkQueueLength(); static double GetWorkQueueRate(); static void SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const intrusive_ptr& origin, const String& path); private: - int m_ID; String m_Identity; bool m_Authenticated; Endpoint::Ptr m_Endpoint; - TlsStream::Ptr m_Stream; + std::shared_ptr m_Stream; ConnectionRole m_Role; double m_Timestamp; double m_Seen; double m_NextHeartbeat; - double m_HeartbeatTimeout; - boost::mutex m_DataHandlerMutex; + boost::asio::io_service::strand m_IoStrand; + std::vector m_OutgoingMessagesQueue; + AsioConditionVariable m_OutgoingMessagesQueued; + AsioConditionVariable m_WriterDone; + bool m_ShuttingDown; - StreamReadContext m_Context; + void HandleIncomingMessages(boost::asio::yield_context yc); + void WriteOutgoingMessages(boost::asio::yield_context yc); + void HandleAndWriteHeartbeats(boost::asio::yield_context yc); + void CheckLiveness(boost::asio::yield_context yc); bool ProcessMessage(); - void MessageHandlerWrapper(const String& jsonString); void MessageHandler(const String& jsonString); - void DataAvailableHandler(); - - static void StaticInitialize(); - static void TimeoutTimerHandler(); - void CheckLiveness(); void CertificateRequestResponseHandler(const Dictionary::Ptr& message); + + void SendMessageInternal(const Dictionary::Ptr& request); }; } diff --git a/lib/remote/modifyobjecthandler.cpp b/lib/remote/modifyobjecthandler.cpp index 8e93f7423..2150903a1 100644 --- a/lib/remote/modifyobjecthandler.cpp +++ b/lib/remote/modifyobjecthandler.cpp @@ -12,15 +12,26 @@ using namespace icinga; REGISTER_URLHANDLER("/v1/objects", ModifyObjectHandler); -bool ModifyObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool ModifyObjectHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() < 3 || request.RequestUrl->GetPath().size() > 4) + namespace http = boost::beast::http; + + if (url->GetPath().size() < 3 || url->GetPath().size() > 4) return false; - if (request.RequestMethod != "POST") + if (request.method() != http::verb::post) return false; - Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); + Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]); if (!type) { HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); @@ -33,10 +44,10 @@ bool ModifyObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r params->Set("type", type->GetName()); - if (request.RequestUrl->GetPath().size() >= 4) { + if (url->GetPath().size() >= 4) { String attr = type->GetName(); boost::algorithm::to_lower(attr); - params->Set(attr, request.RequestUrl->GetPath()[3]); + params->Set(attr, url->GetPath()[3]); } std::vector objs; @@ -101,7 +112,7 @@ bool ModifyObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r { "results", new Array(std::move(results)) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); return true; diff --git a/lib/remote/modifyobjecthandler.hpp b/lib/remote/modifyobjecthandler.hpp index 2df23656c..c9bb21612 100644 --- a/lib/remote/modifyobjecthandler.hpp +++ b/lib/remote/modifyobjecthandler.hpp @@ -13,8 +13,16 @@ class ModifyObjectHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(ModifyObjectHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/objectqueryhandler.cpp b/lib/remote/objectqueryhandler.cpp index 132e2bddd..2d9b79845 100644 --- a/lib/remote/objectqueryhandler.cpp +++ b/lib/remote/objectqueryhandler.cpp @@ -87,15 +87,26 @@ Dictionary::Ptr ObjectQueryHandler::SerializeObjectAttrs(const Object::Ptr& obje return new Dictionary(std::move(resultAttrs)); } -bool ObjectQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool ObjectQueryHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() < 3 || request.RequestUrl->GetPath().size() > 4) + namespace http = boost::beast::http; + + if (url->GetPath().size() < 3 || url->GetPath().size() > 4) return false; - if (request.RequestMethod != "GET") + if (request.method() != http::verb::get) return false; - Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); + Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]); if (!type) { HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); @@ -136,10 +147,10 @@ bool ObjectQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& re params->Set("type", type->GetName()); - if (request.RequestUrl->GetPath().size() >= 4) { + if (url->GetPath().size() >= 4) { String attr = type->GetName(); boost::algorithm::to_lower(attr); - params->Set(attr, request.RequestUrl->GetPath()[3]); + params->Set(attr, url->GetPath()[3]); } std::vector objs; @@ -265,7 +276,7 @@ bool ObjectQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& re { "results", new Array(std::move(results)) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); return true; diff --git a/lib/remote/objectqueryhandler.hpp b/lib/remote/objectqueryhandler.hpp index 634775c64..c35ff4066 100644 --- a/lib/remote/objectqueryhandler.hpp +++ b/lib/remote/objectqueryhandler.hpp @@ -13,8 +13,16 @@ class ObjectQueryHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(ObjectQueryHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; private: static Dictionary::Ptr SerializeObjectAttrs(const Object::Ptr& object, const String& attrPrefix, diff --git a/lib/remote/statushandler.cpp b/lib/remote/statushandler.cpp index e40a7ae03..cc725e4f4 100644 --- a/lib/remote/statushandler.cpp +++ b/lib/remote/statushandler.cpp @@ -68,12 +68,23 @@ public: } }; -bool StatusHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool StatusHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() > 3) + namespace http = boost::beast::http; + + if (url->GetPath().size() > 3) return false; - if (request.RequestMethod != "GET") + if (request.method() != http::verb::get) return false; QueryDescription qd; @@ -83,8 +94,8 @@ bool StatusHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request params->Set("type", "Status"); - if (request.RequestUrl->GetPath().size() >= 3) - params->Set("status", request.RequestUrl->GetPath()[2]); + if (url->GetPath().size() >= 3) + params->Set("status", url->GetPath()[2]); std::vector objs; @@ -101,7 +112,7 @@ bool StatusHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request { "results", new Array(std::move(objs)) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); return true; diff --git a/lib/remote/statushandler.hpp b/lib/remote/statushandler.hpp index a4ec52875..36f5efcf1 100644 --- a/lib/remote/statushandler.hpp +++ b/lib/remote/statushandler.hpp @@ -13,8 +13,16 @@ class StatusHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(StatusHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/templatequeryhandler.cpp b/lib/remote/templatequeryhandler.cpp index ea24fd20f..38a225540 100644 --- a/lib/remote/templatequeryhandler.cpp +++ b/lib/remote/templatequeryhandler.cpp @@ -75,15 +75,26 @@ public: } }; -bool TemplateQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool TemplateQueryHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() < 3 || request.RequestUrl->GetPath().size() > 4) + namespace http = boost::beast::http; + + if (url->GetPath().size() < 3 || url->GetPath().size() > 4) return false; - if (request.RequestMethod != "GET") + if (request.method() != http::verb::get) return false; - Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); + Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]); if (!type) { HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); @@ -97,10 +108,10 @@ bool TemplateQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& params->Set("type", type->GetName()); - if (request.RequestUrl->GetPath().size() >= 4) { + if (url->GetPath().size() >= 4) { String attr = type->GetName(); boost::algorithm::to_lower(attr); - params->Set(attr, request.RequestUrl->GetPath()[3]); + params->Set(attr, url->GetPath()[3]); } std::vector objs; @@ -118,7 +129,7 @@ bool TemplateQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& { "results", new Array(std::move(objs)) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); return true; diff --git a/lib/remote/templatequeryhandler.hpp b/lib/remote/templatequeryhandler.hpp index 4e137e943..e9dec8896 100644 --- a/lib/remote/templatequeryhandler.hpp +++ b/lib/remote/templatequeryhandler.hpp @@ -13,8 +13,16 @@ class TemplateQueryHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(TemplateQueryHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/typequeryhandler.cpp b/lib/remote/typequeryhandler.cpp index e3972616a..077113774 100644 --- a/lib/remote/typequeryhandler.cpp +++ b/lib/remote/typequeryhandler.cpp @@ -46,12 +46,23 @@ public: } }; -bool TypeQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool TypeQueryHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() > 3) + namespace http = boost::beast::http; + + if (url->GetPath().size() > 3) return false; - if (request.RequestMethod != "GET") + if (request.method() != http::verb::get) return false; QueryDescription qd; @@ -64,8 +75,8 @@ bool TypeQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& requ params->Set("type", "Type"); - if (request.RequestUrl->GetPath().size() >= 3) - params->Set("name", request.RequestUrl->GetPath()[2]); + if (url->GetPath().size() >= 3) + params->Set("name", url->GetPath()[2]); std::vector objs; @@ -138,7 +149,7 @@ bool TypeQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& requ { "results", new Array(std::move(results)) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); return true; diff --git a/lib/remote/typequeryhandler.hpp b/lib/remote/typequeryhandler.hpp index 4ca3fcf4e..84c04185d 100644 --- a/lib/remote/typequeryhandler.hpp +++ b/lib/remote/typequeryhandler.hpp @@ -13,8 +13,16 @@ class TypeQueryHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(TypeQueryHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/lib/remote/variablequeryhandler.cpp b/lib/remote/variablequeryhandler.cpp index 04d014784..05b503783 100644 --- a/lib/remote/variablequeryhandler.cpp +++ b/lib/remote/variablequeryhandler.cpp @@ -56,12 +56,23 @@ public: } }; -bool VariableQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response, const Dictionary::Ptr& params) +bool VariableQueryHandler::HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming +) { - if (request.RequestUrl->GetPath().size() > 3) + namespace http = boost::beast::http; + + if (url->GetPath().size() > 3) return false; - if (request.RequestMethod != "GET") + if (request.method() != http::verb::get) return false; QueryDescription qd; @@ -71,8 +82,8 @@ bool VariableQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& params->Set("type", "Variable"); - if (request.RequestUrl->GetPath().size() >= 3) - params->Set("variable", request.RequestUrl->GetPath()[2]); + if (url->GetPath().size() >= 3) + params->Set("variable", url->GetPath()[2]); std::vector objs; @@ -99,7 +110,7 @@ bool VariableQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& { "results", new Array(std::move(results)) } }); - response.SetStatus(200, "OK"); + response.result(http::status::ok); HttpUtility::SendJsonBody(response, params, result); return true; diff --git a/lib/remote/variablequeryhandler.hpp b/lib/remote/variablequeryhandler.hpp index 85a006a4b..ecc71a96a 100644 --- a/lib/remote/variablequeryhandler.hpp +++ b/lib/remote/variablequeryhandler.hpp @@ -13,8 +13,16 @@ class VariableQueryHandler final : public HttpHandler public: DECLARE_PTR_TYPEDEFS(VariableQueryHandler); - bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, - HttpResponse& response, const Dictionary::Ptr& params) override; + bool HandleRequest( + AsioTlsStream& stream, + const ApiUser::Ptr& user, + boost::beast::http::request& request, + const Url::Ptr& url, + boost::beast::http::response& response, + const Dictionary::Ptr& params, + boost::asio::yield_context& yc, + bool& hasStartedStreaming + ) override; }; } diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index ebf0c77df..27fddecff 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -9,7 +9,6 @@ add_executable(check_nscp_api target_link_libraries(check_nscp_api ${base_DEPS}) set_target_properties ( check_nscp_api PROPERTIES - INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2 DEFINE_SYMBOL I2_PLUGINS_BUILD FOLDER Plugins) @@ -32,7 +31,6 @@ if (WIN32) set_target_properties( thresholds PROPERTIES - INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2 FOLDER Plugins ) @@ -50,7 +48,6 @@ if (WIN32) set_target_properties( ${check_OUT} PROPERTIES - INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2 DEFINE_SYMBOL I2_PLUGINS_BUILD FOLDER Plugins )