Merge pull request #7005 from Icinga/feature/boost-asio

ApiListener: use Boost ASIO and coroutines for net I/O
This commit is contained in:
Michael Friedrich 2019-04-01 16:49:44 +02:00 committed by GitHub
commit dc0288fef8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 2414 additions and 1016 deletions

View File

@ -1,50 +1,42 @@
dist: trusty dist: xenial
sudo: false sudo: false
language: cpp language: cpp
cache: ccache 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: addons:
apt_packages: apt:
- libboost-all-dev 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 - flex
- bison - bison
- libssl-dev - libssl-dev
- libpq-dev - libpq-dev
- libmysqlclient-dev - libmysqlclient-dev
- libedit-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
before_script: before_script:
- if [ "$COVERITY_SCAN_BRANCH" != 1 ]; then - arch=$(uname -m)
mkdir build && - mkdir build
cd build && - cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/tmp/icinga2 -DICINGA2_PLUGINDIR=/tmp/icinga2/sbin; - >
fi 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: script:
- if [ "$COVERITY_SCAN_BRANCH" != 1 ]; then - make
make && - make test
make test && - make install
make install && - /tmp/icinga2/sbin/icinga2 --version
/tmp/icinga2/sbin/icinga2 --version && - /tmp/icinga2/sbin/icinga2 daemon -C -DRunAsUser=$(id -u -n) -DRunAsGroup=$(id -g -n)
/tmp/icinga2/sbin/icinga2 daemon -C -DRunAsUser=$(id -u -n) -DRunAsGroup=$(id -g -n);
fi

View File

@ -1,7 +1,7 @@
# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ # Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
cmake_minimum_required(VERSION 2.8.8) cmake_minimum_required(VERSION 2.8.8)
set(BOOST_MIN_VERSION "1.53.0") set(BOOST_MIN_VERSION "1.66.0")
project(icinga2) project(icinga2)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 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}") set(LOGROTATE_USE_SU "\n\tsu ${ICINGA2_USER} ${ICINGA2_GROUP}")
endif() 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}) link_directories(${Boost_LIBRARY_DIRS})
include_directories(${Boost_INCLUDE_DIRS}) include_directories(${Boost_INCLUDE_DIRS})
@ -190,6 +195,7 @@ if(WIN32)
endif() endif()
set(CMAKE_MACOSX_RPATH 1) set(CMAKE_MACOSX_RPATH 1)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_RPATH};${CMAKE_INSTALL_FULL_LIBDIR}/icinga2")
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Qunused-arguments -fcolor-diagnostics") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Qunused-arguments -fcolor-diagnostics")

View File

@ -7,8 +7,9 @@ platform: x64
environment: environment:
CMAKE_GENERATOR: "Visual Studio 15 2017 Win64" CMAKE_GENERATOR: "Visual Studio 15 2017 Win64"
VSCMD_VER: 15.0 VSCMD_VER: 15.0
BOOST_ROOT: 'C:\Libraries\boost_1_65_1' # https://www.appveyor.com/docs/windows-images-software/#boost
BOOST_LIBRARYDIR: 'C:\Libraries\boost_1_65_1\lib64-msvc-14.1' 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' BISON_BINARY: 'C:\ProgramData\chocolatey\lib\winflexbison3\tools\win_bison.exe'
FLEX_BINARY: 'C:\ProgramData\chocolatey\lib\winflexbison3\tools\win_flex.exe' FLEX_BINARY: 'C:\ProgramData\chocolatey\lib\winflexbison3\tools\win_flex.exe'
CMAKE_BUILD_TYPE: Debug CMAKE_BUILD_TYPE: Debug

View File

@ -57,7 +57,7 @@ Configuration Attributes:
ca\_path | String | **Deprecated.** Path to the CA certificate file. 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. 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. 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`. 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\_config | Boolean | **Optional.** Accept zone configuration. Defaults to `false`.
accept\_commands | Boolean | **Optional.** Accept remote commands. Defaults to `false`. accept\_commands | Boolean | **Optional.** Accept remote commands. Defaults to `false`.

View File

@ -1095,8 +1095,8 @@ Icinga application using a dist tarball (including notes for distributions):
- SUSE: libopenssl-devel (for SLES 11: libopenssl1-devel) - SUSE: libopenssl-devel (for SLES 11: libopenssl1-devel)
- Debian/Ubuntu: libssl-dev - Debian/Ubuntu: libssl-dev
- Alpine: libressl-dev - Alpine: libressl-dev
* Boost library and header files >= 1.53.0 * Boost library and header files >= 1.66.0
- RHEL/Fedora: boost153-devel - RHEL/Fedora: boost166-devel
- Debian/Ubuntu: libboost-all-dev - Debian/Ubuntu: libboost-all-dev
- Alpine: boost-dev - Alpine: boost-dev
* GNU bison (bison) * GNU bison (bison)

View File

@ -68,7 +68,6 @@ target_link_libraries(icinga-app ${base_DEPS})
set_target_properties ( set_target_properties (
icinga-app PROPERTIES icinga-app PROPERTIES
INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2
FOLDER Bin FOLDER Bin
OUTPUT_NAME icinga2 OUTPUT_NAME icinga2
) )

View File

@ -34,7 +34,9 @@ set(base_SOURCES
filelogger.cpp filelogger.hpp filelogger-ti.hpp filelogger.cpp filelogger.hpp filelogger-ti.hpp
function.cpp function.hpp function-ti.hpp function-script.cpp functionwrapper.hpp function.cpp function.hpp function-ti.hpp function-script.cpp functionwrapper.hpp
initialize.cpp initialize.hpp initialize.cpp initialize.hpp
io-engine.cpp io-engine.hpp
json.cpp json.hpp json-script.cpp json.cpp json.hpp json-script.cpp
lazy-init.hpp
library.cpp library.hpp library.cpp library.hpp
loader.cpp loader.hpp loader.cpp loader.hpp
logger.cpp logger.hpp logger-ti.hpp logger.cpp logger.hpp logger-ti.hpp

162
lib/base/io-engine.cpp Normal file
View File

@ -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 <exception>
#include <memory>
#include <thread>
#include <boost/asio/io_service.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/system/error_code.hpp>
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<std::unique_ptr<IoEngine>> IoEngine::m_Instance ([]() { return std::unique_ptr<IoEngine>(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]);
}

135
lib/base/io-engine.hpp Normal file
View File

@ -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 <atomic>
#include <exception>
#include <memory>
#include <thread>
#include <vector>
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/io_service.hpp>
#include <boost/asio/spawn.hpp>
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<std::unique_ptr<IoEngine>> m_Instance;
boost::asio::io_service m_IoService;
boost::asio::io_service::work m_KeepAlive;
std::vector<std::thread> 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 */

89
lib/base/lazy-init.hpp Normal file
View File

@ -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 <atomic>
#include <functional>
#include <mutex>
#include <utility>
namespace icinga
{
/**
* Lazy object initialization abstraction inspired from
* <https://docs.microsoft.com/en-us/dotnet/api/system.lazy-1?view=netframework-4.7.2>.
*
* @ingroup base
*/
template<class T>
class LazyInit
{
public:
inline
LazyInit(std::function<T()> 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<std::mutex> 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<T()> m_Initializer;
std::mutex m_Mutex;
std::atomic<T*> m_Underlying;
};
}
#endif /* LAZY_INIT */

View File

@ -2,7 +2,15 @@
#include "base/netstring.hpp" #include "base/netstring.hpp"
#include "base/debug.hpp" #include "base/debug.hpp"
#include "base/tlsstream.hpp"
#include <cstdint>
#include <memory>
#include <sstream> #include <sstream>
#include <utility>
#include <boost/asio/buffer.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/write.hpp>
using namespace icinga; using namespace icinga;
@ -110,6 +118,108 @@ size_t NetString::WriteStringToStream(const Stream::Ptr& stream, const String& s
return msg.GetLength(); 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<AsioTlsStream>& 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<AsioTlsStream>& 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. * Writes data into a stream using the netstring format.
* *

View File

@ -5,6 +5,9 @@
#include "base/i2-base.hpp" #include "base/i2-base.hpp"
#include "base/stream.hpp" #include "base/stream.hpp"
#include "base/tlsstream.hpp"
#include <memory>
#include <boost/asio/spawn.hpp>
namespace icinga namespace icinga
{ {
@ -23,7 +26,10 @@ class NetString
public: public:
static StreamReadStatus ReadStringFromStream(const Stream::Ptr& stream, String *message, StreamReadContext& context, static StreamReadStatus ReadStringFromStream(const Stream::Ptr& stream, String *message, StreamReadContext& context,
bool may_wait = false, ssize_t maxMessageLength = -1); bool may_wait = false, ssize_t maxMessageLength = -1);
static String ReadStringFromStream(const std::shared_ptr<AsioTlsStream>& 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 Stream::Ptr& stream, const String& message);
static size_t WriteStringToStream(const std::shared_ptr<AsioTlsStream>& stream, const String& message, boost::asio::yield_context yc);
static void WriteStringToStream(std::ostream& stream, const String& message); static void WriteStringToStream(std::ostream& stream, const String& message);
private: private:

View File

@ -5,6 +5,8 @@
#include "base/i2-base.hpp" #include "base/i2-base.hpp"
#include "base/socket.hpp" #include "base/socket.hpp"
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
namespace icinga namespace icinga
{ {
@ -25,6 +27,35 @@ public:
void Connect(const String& node, const String& service); void Connect(const String& node, const String& service);
}; };
template<class Socket>
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 */ #endif /* TCPSOCKET_H */

View File

@ -1,12 +1,20 @@
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
#include "base/application.hpp"
#include "base/tlsstream.hpp" #include "base/tlsstream.hpp"
#include "base/utility.hpp" #include "base/utility.hpp"
#include "base/exception.hpp" #include "base/exception.hpp"
#include "base/logger.hpp" #include "base/logger.hpp"
#include "base/configuration.hpp" #include "base/configuration.hpp"
#include "base/convert.hpp" #include "base/convert.hpp"
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/asio/ssl/verify_mode.hpp>
#include <iostream> #include <iostream>
#include <openssl/ssl.h>
#include <openssl/tls1.h>
#include <openssl/x509.h>
#include <sstream>
#ifndef _WIN32 #ifndef _WIN32
# include <poll.h> # include <poll.h>
@ -26,6 +34,28 @@ bool TlsStream::m_SSLIndexInitialized = false;
* @param sslContext The SSL context for 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<SSL_CTX>& sslContext) TlsStream::TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const std::shared_ptr<SSL_CTX>& 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<boost::asio::ssl::context>& 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), : 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_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) 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; std::ostringstream msgbuf;
char errbuf[120]; char errbuf[120];
m_SSL = std::shared_ptr<SSL>(SSL_new(sslContext.get()), SSL_free); m_SSL = std::shared_ptr<SSL>(SSL_new(sslContext), SSL_free);
if (!m_SSL) { if (!m_SSL) {
msgbuf << "SSL_new() failed with code " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; 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; 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 */
}

View File

@ -9,6 +9,12 @@
#include "base/stream.hpp" #include "base/stream.hpp"
#include "base/tlsutility.hpp" #include "base/tlsutility.hpp"
#include "base/fifo.hpp" #include "base/fifo.hpp"
#include <utility>
#include <boost/asio/buffered_stream.hpp>
#include <boost/asio/io_service.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
namespace icinga namespace icinga
{ {
@ -32,6 +38,7 @@ public:
DECLARE_PTR_TYPEDEFS(TlsStream); DECLARE_PTR_TYPEDEFS(TlsStream);
TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const std::shared_ptr<SSL_CTX>& sslContext = MakeSSLContext()); TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const std::shared_ptr<SSL_CTX>& sslContext = MakeSSLContext());
TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const std::shared_ptr<boost::asio::ssl::context>& sslContext);
~TlsStream() override; ~TlsStream() override;
Socket::Ptr GetSocket() const; Socket::Ptr GetSocket() const;
@ -80,6 +87,8 @@ private:
static int m_SSLIndex; static int m_SSLIndex;
static bool m_SSLIndexInitialized; static bool m_SSLIndexInitialized;
TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, SSL_CTX* sslContext);
void OnEvent(int revents) override; void OnEvent(int revents) override;
void HandleError() const; void HandleError() const;
@ -90,6 +99,70 @@ private:
void CloseInternal(bool inDestructor); void CloseInternal(bool inDestructor);
}; };
struct UnbufferedAsioTlsStreamParams
{
boost::asio::io_service& IoService;
boost::asio::ssl::context& SslContext;
const String& Hostname;
};
typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> 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<class... Args>
inline
auto async_handshake(handshake_type type, Args&&... args) -> decltype(((AsioTcpTlsStream*)nullptr)->async_handshake(type, std::forward<Args>(args)...))
{
BeforeHandshake(type);
return AsioTcpTlsStream::async_handshake(type, std::forward<Args>(args)...);
}
template<class... Args>
inline
auto handshake(handshake_type type, Args&&... args) -> decltype(((AsioTcpTlsStream*)nullptr)->handshake(type, std::forward<Args>(args)...))
{
BeforeHandshake(type);
return AsioTcpTlsStream::handshake(type, std::forward<Args>(args)...);
}
private:
bool m_VerifyOK;
String m_VerifyError;
String m_Hostname;
void BeforeHandshake(handshake_type type);
};
class AsioTlsStream : public boost::asio::buffered_stream<UnbufferedAsioTlsStream>
{
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 */ #endif /* TLSSTREAM_H */

View File

@ -7,6 +7,7 @@
#include "base/utility.hpp" #include "base/utility.hpp"
#include "base/application.hpp" #include "base/application.hpp"
#include "base/exception.hpp" #include "base/exception.hpp"
#include <boost/asio/ssl/context.hpp>
#include <fstream> #include <fstream>
namespace icinga namespace icinga
@ -57,35 +58,23 @@ void InitializeOpenSSL()
l_SSLInitialized = true; l_SSLInitialized = true;
} }
/** static void SetupSslContext(SSL_CTX *sslContext, const String& pubkey, const String& privkey, const String& cakey)
* 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<SSL_CTX> MakeSSLContext(const String& pubkey, const String& privkey, const String& cakey)
{ {
char errbuf[120]; char errbuf[120];
InitializeOpenSSL();
std::shared_ptr<SSL_CTX> sslContext = std::shared_ptr<SSL_CTX>(SSL_CTX_new(SSLv23_method()), SSL_CTX_free);
long flags = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_CIPHER_SERVER_PREFERENCE; long flags = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_CIPHER_SERVER_PREFERENCE;
#ifdef SSL_OP_NO_COMPRESSION #ifdef SSL_OP_NO_COMPRESSION
flags |= SSL_OP_NO_COMPRESSION; flags |= SSL_OP_NO_COMPRESSION;
#endif /* 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_mode(sslContext, 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_session_id_context(sslContext, (const unsigned char *)"Icinga 2", 8);
if (!pubkey.IsEmpty()) { 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") Log(LogCritical, "SSL")
<< "Error with public key file '" << pubkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; << "Error with public key file '" << pubkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\"";
BOOST_THROW_EXCEPTION(openssl_error() BOOST_THROW_EXCEPTION(openssl_error()
@ -96,7 +85,7 @@ std::shared_ptr<SSL_CTX> MakeSSLContext(const String& pubkey, const String& priv
} }
if (!privkey.IsEmpty()) { 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") Log(LogCritical, "SSL")
<< "Error with private key file '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; << "Error with private key file '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\"";
BOOST_THROW_EXCEPTION(openssl_error() BOOST_THROW_EXCEPTION(openssl_error()
@ -105,7 +94,7 @@ std::shared_ptr<SSL_CTX> MakeSSLContext(const String& pubkey, const String& priv
<< boost::errinfo_file_name(privkey)); << boost::errinfo_file_name(privkey));
} }
if (!SSL_CTX_check_private_key(sslContext.get())) { if (!SSL_CTX_check_private_key(sslContext)) {
Log(LogCritical, "SSL") Log(LogCritical, "SSL")
<< "Error checking private key '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; << "Error checking private key '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\"";
BOOST_THROW_EXCEPTION(openssl_error() BOOST_THROW_EXCEPTION(openssl_error()
@ -115,7 +104,7 @@ std::shared_ptr<SSL_CTX> MakeSSLContext(const String& pubkey, const String& priv
} }
if (!cakey.IsEmpty()) { 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") Log(LogCritical, "SSL")
<< "Error loading and verifying locations in ca key file '" << cakey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; << "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() BOOST_THROW_EXCEPTION(openssl_error()
@ -136,22 +125,60 @@ std::shared_ptr<SSL_CTX> MakeSSLContext(const String& pubkey, const String& priv
<< boost::errinfo_file_name(cakey)); << 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<SSL_CTX> MakeSSLContext(const String& pubkey, const String& privkey, const String& cakey)
{
InitializeOpenSSL();
std::shared_ptr<SSL_CTX> sslContext = std::shared_ptr<SSL_CTX>(SSL_CTX_new(SSLv23_method()), SSL_CTX_free);
SetupSslContext(sslContext.get(), pubkey, privkey, cakey);
return sslContext; 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<boost::asio::ssl::context> MakeAsioSslContext(const String& pubkey, const String& privkey, const String& cakey)
{
namespace ssl = boost::asio::ssl;
InitializeOpenSSL();
auto context (std::make_shared<ssl::context>(ssl::context::sslv23));
SetupSslContext(context->native_handle(), pubkey, privkey, cakey);
return context;
}
/** /**
* Set the cipher list to the specified SSL context. * Set the cipher list to the specified SSL context.
* @param context The ssl context. * @param context The ssl context.
* @param cipherList The ciper list. * @param cipherList The ciper list.
**/ **/
void SetCipherListToSSLContext(const std::shared_ptr<SSL_CTX>& context, const String& cipherList) void SetCipherListToSSLContext(const std::shared_ptr<boost::asio::ssl::context>& context, const String& cipherList)
{ {
char errbuf[256]; 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") Log(LogCritical, "SSL")
<< "Cipher list '" << "Cipher list '"
<< cipherList << cipherList
@ -171,9 +198,9 @@ void SetCipherListToSSLContext(const std::shared_ptr<SSL_CTX>& context, const St
* @param context The ssl context. * @param context The ssl context.
* @param tlsProtocolmin The minimum TLS protocol version. * @param tlsProtocolmin The minimum TLS protocol version.
*/ */
void SetTlsProtocolminToSSLContext(const std::shared_ptr<SSL_CTX>& context, const String& tlsProtocolmin) void SetTlsProtocolminToSSLContext(const std::shared_ptr<boost::asio::ssl::context>& 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; flags |= SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3;
@ -190,7 +217,7 @@ void SetTlsProtocolminToSSLContext(const std::shared_ptr<SSL_CTX>& context, cons
if (tlsProtocolmin != SSL_TXT_TLSV1) if (tlsProtocolmin != SSL_TXT_TLSV1)
BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid TLS protocol version specified.")); 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<SSL_CTX>& context, cons
* @param context The SSL context. * @param context The SSL context.
* @param crlPath The path to the CRL file. * @param crlPath The path to the CRL file.
*/ */
void AddCRLToSSLContext(const std::shared_ptr<SSL_CTX>& context, const String& crlPath) void AddCRLToSSLContext(const std::shared_ptr<boost::asio::ssl::context>& context, const String& crlPath)
{ {
char errbuf[120]; 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; X509_LOOKUP *lookup;
lookup = X509_STORE_add_lookup(x509_store, X509_LOOKUP_file()); lookup = X509_STORE_add_lookup(x509_store, X509_LOOKUP_file());

View File

@ -14,6 +14,7 @@
#include <openssl/x509v3.h> #include <openssl/x509v3.h>
#include <openssl/evp.h> #include <openssl/evp.h>
#include <openssl/rand.h> #include <openssl/rand.h>
#include <boost/asio/ssl/context.hpp>
#include <boost/exception/info.hpp> #include <boost/exception/info.hpp>
namespace icinga namespace icinga
@ -21,9 +22,10 @@ namespace icinga
void InitializeOpenSSL(); void InitializeOpenSSL();
std::shared_ptr<SSL_CTX> MakeSSLContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String()); std::shared_ptr<SSL_CTX> MakeSSLContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String());
void AddCRLToSSLContext(const std::shared_ptr<SSL_CTX>& context, const String& crlPath); std::shared_ptr<boost::asio::ssl::context> MakeAsioSslContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String());
void SetCipherListToSSLContext(const std::shared_ptr<SSL_CTX>& context, const String& cipherList); void AddCRLToSSLContext(const std::shared_ptr<boost::asio::ssl::context>& context, const String& crlPath);
void SetTlsProtocolminToSSLContext(const std::shared_ptr<SSL_CTX>& context, const String& tlsProtocolmin); void SetCipherListToSSLContext(const std::shared_ptr<boost::asio::ssl::context>& context, const String& cipherList);
void SetTlsProtocolminToSSLContext(const std::shared_ptr<boost::asio::ssl::context>& context, const String& tlsProtocolmin);
String GetCertificateCN(const std::shared_ptr<X509>& certificate); String GetCertificateCN(const std::shared_ptr<X509>& certificate);
std::shared_ptr<X509> GetX509Certificate(const String& pemfile); std::shared_ptr<X509> GetX509Certificate(const String& pemfile);
int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile = String(), const String& certfile = String(), bool ca = false); int MakeX509CSR(const String& cn, const String& keyfile, const String& csrfile = String(), const String& certfile = String(), bool ca = false);

View File

@ -12,15 +12,26 @@ using namespace icinga;
REGISTER_URLHANDLER("/v1/actions", ActionsHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() != 3) namespace http = boost::beast::http;
if (url->GetPath().size() != 3)
return false; return false;
if (request.RequestMethod != "POST") if (request.method() != http::verb::post)
return false; return false;
String actionName = request.RequestUrl->GetPath()[2]; String actionName = url->GetPath()[2];
ApiAction::Ptr action = ApiAction::GetByName(actionName); ApiAction::Ptr action = ApiAction::GetByName(actionName);
@ -81,17 +92,15 @@ bool ActionsHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& reques
} }
int statusCode = 500; int statusCode = 500;
String statusMessage = "No action executed successfully";
for (const Dictionary::Ptr& res : results) { for (const Dictionary::Ptr& res : results) {
if (res->Contains("code") && res->Get("code") == 200) { if (res->Contains("code") && res->Get("code") == 200) {
statusCode = 200; statusCode = 200;
statusMessage = "OK";
break; break;
} }
} }
response.SetStatus(statusCode, statusMessage); response.result(statusCode);
Dictionary::Ptr result = new Dictionary({ Dictionary::Ptr result = new Dictionary({
{ "results", new Array(std::move(results)) } { "results", new Array(std::move(results)) }

View File

@ -13,8 +13,16 @@ class ActionsHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(ActionsHandler); DECLARE_PTR_TYPEDEFS(ActionsHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -323,7 +323,7 @@ void ApiListener::UpdateConfigObject(const ConfigObject::Ptr& object, const Mess
#endif /* I2_DEBUG */ #endif /* I2_DEBUG */
if (client) if (client)
JsonRpc::SendMessage(client->GetStream(), message); client->SendMessage(message);
else { else {
Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone()); Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone());
@ -373,7 +373,7 @@ void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const Mess
#endif /* I2_DEBUG */ #endif /* I2_DEBUG */
if (client) if (client)
JsonRpc::SendMessage(client->GetStream(), message); client->SendMessage(message);
else { else {
Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone()); Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone());

View File

@ -7,6 +7,8 @@
#include "remote/jsonrpc.hpp" #include "remote/jsonrpc.hpp"
#include "remote/apifunction.hpp" #include "remote/apifunction.hpp"
#include "base/convert.hpp" #include "base/convert.hpp"
#include "base/defer.hpp"
#include "base/io-engine.hpp"
#include "base/netstring.hpp" #include "base/netstring.hpp"
#include "base/json.hpp" #include "base/json.hpp"
#include "base/configtype.hpp" #include "base/configtype.hpp"
@ -18,7 +20,19 @@
#include "base/context.hpp" #include "base/context.hpp"
#include "base/statsfunction.hpp" #include "base/statsfunction.hpp"
#include "base/exception.hpp" #include "base/exception.hpp"
#include "base/tcpsocket.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/system/error_code.hpp>
#include <climits>
#include <fstream> #include <fstream>
#include <memory>
#include <openssl/ssl.h>
#include <openssl/tls1.h>
#include <openssl/x509.h>
#include <sstream>
using namespace icinga; 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<void ()>& callback, SchedulerPolicy policy)
{
GetTP().Post(callback, policy);
}
void ApiListener::OnConfigLoaded() void ApiListener::OnConfigLoaded()
{ {
if (m_Instance) if (m_Instance)
@ -159,10 +157,12 @@ void ApiListener::OnConfigLoaded()
void ApiListener::UpdateSSLContext() void ApiListener::UpdateSSLContext()
{ {
std::shared_ptr<SSL_CTX> context; namespace ssl = boost::asio::ssl;
std::shared_ptr<ssl::context> context;
try { try {
context = MakeSSLContext(GetDefaultCertPath(), GetDefaultKeyPath(), GetDefaultCaPath()); context = MakeAsioSslContext(GetDefaultCertPath(), GetDefaultKeyPath(), GetDefaultCaPath());
} catch (const std::exception&) { } catch (const std::exception&) {
BOOST_THROW_EXCEPTION(ScriptError("Cannot make SSL context for cert path: '" BOOST_THROW_EXCEPTION(ScriptError("Cannot make SSL context for cert path: '"
+ GetDefaultCertPath() + "' key path: '" + GetDefaultKeyPath() + "' ca path: '" + GetDefaultCaPath() + "'.", GetDebugInfo())); + 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) bool ApiListener::AddListener(const String& node, const String& service)
{ {
namespace asio = boost::asio;
namespace ip = asio::ip;
using ip::tcp;
ObjectLock olock(this); ObjectLock olock(this);
std::shared_ptr<SSL_CTX> sslContext = m_SSLContext; auto sslContext (m_SSLContext);
if (!sslContext) { if (!sslContext) {
Log(LogCritical, "ApiListener", "SSL context is required for AddListener()"); Log(LogCritical, "ApiListener", "SSL context is required for AddListener()");
return false; return false;
} }
TcpSocket::Ptr server = new TcpSocket(); auto& io (IoEngine::Get().GetIoService());
auto acceptor (std::make_shared<tcp::acceptor>(io));
try { try {
server->Bind(node, service, AF_UNSPEC); 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<const char *>(&optFalse), sizeof(optFalse));
const int optTrue = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char *>(&optTrue), sizeof(optTrue));
#ifndef _WIN32
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast<const char *>(&optTrue), sizeof(optTrue));
#endif /* _WIN32 */
}
acceptor->bind(current->endpoint());
break;
} catch (const std::exception&) { } catch (const std::exception&) {
if (++current == result.end()) {
throw;
}
if (acceptor->is_open()) {
acceptor->close();
}
}
}
} catch (const std::exception& ex) {
Log(LogCritical, "ApiListener") 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; return false;
} }
acceptor->listen(INT_MAX);
auto localEndpoint (acceptor->local_endpoint());
Log(LogInformation, "ApiListener") 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)); asio::spawn(io, [this, acceptor, sslContext](asio::yield_context yc) { ListenerCoroutineProc(yc, acceptor, sslContext); });
thread.detach();
m_Servers.insert(server); UpdateStatusFile(localEndpoint);
UpdateStatusFile(server);
return true; return true;
} }
void ApiListener::ListenerThreadProc(const Socket::Ptr& server) void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const std::shared_ptr<boost::asio::ip::tcp::acceptor>& server, const std::shared_ptr<boost::asio::ssl::context>& sslContext)
{ {
Utility::SetThreadName("API Listener"); namespace asio = boost::asio;
server->Listen(); auto& io (server->get_io_service());
for (;;) { for (;;) {
try { try {
Socket::Ptr client = server->Accept(); auto sslConn (std::make_shared<AsioTlsStream>(io, *sslContext));
/* Use dynamic thread pool with additional on demand resources with fast throughput. */ server->async_accept(sslConn->lowest_layer(), yc);
EnqueueAsyncCallback(std::bind(&ApiListener::NewClientHandler, this, client, String(), RoleServer), LowLatencyScheduler);
} catch (const std::exception&) { asio::spawn(io, [this, sslConn](asio::yield_context yc) { NewClientHandler(yc, sslConn, String(), RoleServer); });
Log(LogCritical, "ApiListener", "Cannot accept new connection."); } 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) void ApiListener::AddConnection(const Endpoint::Ptr& endpoint)
{ {
{ namespace asio = boost::asio;
ObjectLock olock(this); using asio::ip::tcp;
std::shared_ptr<SSL_CTX> sslContext = m_SSLContext; auto sslContext (m_SSLContext);
if (!sslContext) { if (!sslContext) {
Log(LogCritical, "ApiListener", "SSL context is required for AddConnection()"); Log(LogCritical, "ApiListener", "SSL context is required for AddConnection()");
return; return;
} }
}
auto& io (IoEngine::Get().GetIoService());
asio::spawn(io, [this, endpoint, &io, sslContext](asio::yield_context yc) {
String host = endpoint->GetHost(); String host = endpoint->GetHost();
String port = endpoint->GetPort(); String port = endpoint->GetPort();
Log(LogInformation, "ApiListener") Log(LogInformation, "ApiListener")
<< "Reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'"; << "Reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'";
TcpSocket::Ptr client = new TcpSocket();
try { try {
client->Connect(host, port); auto sslConn (std::make_shared<AsioTlsStream>(io, *sslContext, endpoint->GetName()));
NewClientHandler(client, endpoint->GetName(), RoleClient); Connect(sslConn->lowest_layer(), host, port, yc);
NewClientHandler(yc, sslConn, endpoint->GetName(), RoleClient);
endpoint->SetConnecting(false); endpoint->SetConnecting(false);
Log(LogInformation, "ApiListener") Log(LogInformation, "ApiListener")
<< "Finished reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'"; << "Finished reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'";
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
endpoint->SetConnecting(false); endpoint->SetConnecting(false);
client->Close();
std::ostringstream info; Log(LogCritical, "ApiListener")
info << "Cannot connect to host '" << host << "' on port '" << port << "'"; << "Cannot connect to host '" << host << "' on port '" << port << "': " << ex.what();
Log(LogCritical, "ApiListener", info.str());
Log(LogDebug, "ApiListener")
<< info.str() << "\n" << DiagnosticInformation(ex);
} }
});
} }
void ApiListener::NewClientHandler(const Socket::Ptr& client, const String& hostname, ConnectionRole role) void ApiListener::NewClientHandler(boost::asio::yield_context yc, const std::shared_ptr<AsioTlsStream>& client, const String& hostname, ConnectionRole role)
{ {
try { try {
NewClientHandlerInternal(client, hostname, role); NewClientHandlerInternal(yc, client, hostname, role);
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
Log(LogCritical, "ApiListener") Log(LogCritical, "ApiListener")
<< "Exception while handling new API client connection: " << DiagnosticInformation(ex, false); << "Exception while handling new API client connection: " << DiagnosticInformation(ex, false);
@ -440,89 +482,89 @@ void ApiListener::NewClientHandler(const Socket::Ptr& client, const String& host
* *
* @param client The new client. * @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<AsioTlsStream>& client, const String& hostname, ConnectionRole role)
{ {
CONTEXT("Handling new API client connection"); namespace asio = boost::asio;
namespace ssl = asio::ssl;
String conninfo; 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); std::ostringstream conninfo_;
try {
tlsStream = new TlsStream(client, serverName, role, m_SSLContext); if (role == RoleClient) {
} catch (const std::exception&) { conninfo_ << "to";
Log(LogCritical, "ApiListener") } else {
<< "Cannot create TLS stream from client connection (" << conninfo << ")"; conninfo_ << "from";
return;
}
} }
auto endpoint (client->lowest_layer().remote_endpoint());
conninfo_ << " [" << endpoint.address() << "]:" << endpoint.port();
conninfo = conninfo_.str();
}
auto& sslConn (client->next_layer());
try { try {
tlsStream->Handshake(); sslConn.async_handshake(role == RoleClient ? sslConn.client : sslConn.server, yc);
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
Log(LogCritical, "ApiListener") Log(LogCritical, "ApiListener")
<< "Client TLS handshake failed (" << conninfo << "): " << DiagnosticInformation(ex, false); << "Client TLS handshake failed (" << conninfo << "): " << DiagnosticInformation(ex, false);
tlsStream->Close();
return; return;
} }
std::shared_ptr<X509> cert = tlsStream->GetPeerCertificate(); bool willBeShutDown = false;
Defer shutDownIfNeeded ([&sslConn, &willBeShutDown, &yc]() {
if (!willBeShutDown) {
sslConn.async_shutdown(yc);
}
});
std::shared_ptr<X509> cert (SSL_get_peer_certificate(sslConn.native_handle()), X509_free);
bool verify_ok = false;
String identity; String identity;
Endpoint::Ptr endpoint; Endpoint::Ptr endpoint;
bool verify_ok = false;
if (cert) { if (cert) {
verify_ok = sslConn.IsVerifyOK();
String verifyError = sslConn.GetVerifyError();
try { try {
identity = GetCertificateCN(cert); identity = GetCertificateCN(cert);
} catch (const std::exception&) { } catch (const std::exception&) {
Log(LogCritical, "ApiListener") Log(LogCritical, "ApiListener")
<< "Cannot get certificate common name from cert path: '" << GetDefaultCertPath() << "'."; << "Cannot get certificate common name from cert path: '" << GetDefaultCertPath() << "'.";
tlsStream->Close();
return; return;
} }
verify_ok = tlsStream->IsVerifyOK();
if (!hostname.IsEmpty()) { if (!hostname.IsEmpty()) {
if (identity != hostname) { if (identity != hostname) {
Log(LogWarning, "ApiListener") Log(LogWarning, "ApiListener")
<< "Unexpected certificate common name while connecting to endpoint '" << "Unexpected certificate common name while connecting to endpoint '"
<< hostname << "': got '" << identity << "'"; << hostname << "': got '" << identity << "'";
tlsStream->Close();
return; return;
} else if (!verify_ok) { } else if (!verify_ok) {
Log(LogWarning, "ApiListener") Log(LogWarning, "ApiListener")
<< "Certificate validation failed for endpoint '" << hostname << "Certificate validation failed for endpoint '" << hostname
<< "': " << tlsStream->GetVerifyError(); << "': " << verifyError;
} }
} }
if (verify_ok) if (verify_ok) {
endpoint = Endpoint::GetByName(identity); 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) if (!verify_ok) {
log << " (certificate validation failed: " << tlsStream->GetVerifyError() << ")"; log << " (certificate validation failed: " << verifyError << ")";
else if (!endpoint) } else if (!endpoint) {
log << " (no Endpoint object found for identity)"; log << " (no Endpoint object found for identity)";
} }
} else { } else {
@ -533,65 +575,84 @@ void ApiListener::NewClientHandlerInternal(const Socket::Ptr& client, const Stri
ClientType ctype; ClientType ctype;
if (role == RoleClient) { if (role == RoleClient) {
Dictionary::Ptr message = new Dictionary({ JsonRpc::SendMessage(client, new Dictionary({
{ "jsonrpc", "2.0" }, { "jsonrpc", "2.0" },
{ "method", "icinga::Hello" }, { "method", "icinga::Hello" },
{ "params", new Dictionary() } { "params", new Dictionary() }
}); }), yc);
client->async_flush(yc);
JsonRpc::SendMessage(tlsStream, message);
ctype = ClientJsonRpc; ctype = ClientJsonRpc;
} else { } else {
tlsStream->WaitForData(10); {
boost::system::error_code ec;
if (!tlsStream->IsDataAvailable()) { if (client->async_fill(yc[ec]) == 0u) {
if (identity.IsEmpty()) if (identity.IsEmpty()) {
Log(LogInformation, "ApiListener") Log(LogInformation, "ApiListener")
<< "No data received on new API connection. " << "No data received on new API connection. "
<< "Ensure that the remote endpoints are properly configured in a cluster setup."; << "Ensure that the remote endpoints are properly configured in a cluster setup.";
else } else {
Log(LogWarning, "ApiListener") Log(LogWarning, "ApiListener")
<< "No data received on new API connection for identity '" << identity << "'. " << "No data received on new API connection for identity '" << identity << "'. "
<< "Ensure that the remote endpoints are properly configured in a cluster setup."; << "Ensure that the remote endpoints are properly configured in a cluster setup.";
tlsStream->Close();
return;
} }
char firstByte; return;
tlsStream->Peek(&firstByte, 1, false); }
}
if (firstByte >= '0' && firstByte <= '9') char firstByte = 0;
{
asio::mutable_buffer firstByteBuf (&firstByte, 1);
client->peek(firstByteBuf);
}
if (firstByte >= '0' && firstByte <= '9') {
ctype = ClientJsonRpc; ctype = ClientJsonRpc;
else } else {
ctype = ClientHttp; ctype = ClientHttp;
} }
}
if (ctype == ClientJsonRpc) { if (ctype == ClientJsonRpc) {
Log(LogNotice, "ApiListener", "New JSON-RPC client"); Log(LogNotice, "ApiListener", "New JSON-RPC client");
JsonRpcConnection::Ptr aclient = new JsonRpcConnection(identity, verify_ok, tlsStream, role); JsonRpcConnection::Ptr aclient = new JsonRpcConnection(identity, verify_ok, client, role);
aclient->Start();
if (endpoint) { if (endpoint) {
bool needSync = !endpoint->GetConnected(); bool needSync = !endpoint->GetConnected();
endpoint->AddClient(aclient); endpoint->AddClient(aclient);
m_SyncQueue.Enqueue(std::bind(&ApiListener::SyncClient, this, aclient, endpoint, needSync)); asio::spawn(client->get_io_service(), [this, aclient, endpoint, needSync](asio::yield_context yc) {
} else { CpuBoundWork syncClient (yc);
if (!AddAnonymousClient(aclient)) {
SyncClient(aclient, endpoint, needSync);
});
} else if (!AddAnonymousClient(aclient)) {
Log(LogNotice, "ApiListener") Log(LogNotice, "ApiListener")
<< "Ignoring anonymous JSON-RPC connection " << conninfo << "Ignoring anonymous JSON-RPC connection " << conninfo
<< ". Max connections (" << GetMaxAnonymousClients() << ") exceeded."; << ". Max connections (" << GetMaxAnonymousClients() << ") exceeded.";
aclient->Disconnect();
aclient = nullptr;
} }
if (aclient) {
aclient->Start();
willBeShutDown = true;
} }
} else { } else {
Log(LogNotice, "ApiListener", "New HTTP client"); Log(LogNotice, "ApiListener", "New HTTP client");
HttpServerConnection::Ptr aclient = new HttpServerConnection(identity, verify_ok, tlsStream); HttpServerConnection::Ptr aclient = new HttpServerConnection(identity, verify_ok, client);
aclient->Start();
AddHttpClient(aclient); AddHttpClient(aclient);
aclient->Start();
willBeShutDown = true;
} }
} }
@ -727,10 +788,11 @@ void ApiListener::ApiTimerHandler()
} }
for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) { for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
if (client->GetTimestamp() != maxTs) if (client->GetTimestamp() == maxTs) {
client->Disconnect();
else
client->SendMessage(lmessage); client->SendMessage(lmessage);
} else {
client->Disconnect();
}
} }
Log(LogNotice, "ApiListener") Log(LogNotice, "ApiListener")
@ -791,8 +853,7 @@ void ApiListener::ApiReconnectTimerHandler()
/* Set connecting state to prevent duplicated queue inserts later. */ /* Set connecting state to prevent duplicated queue inserts later. */
endpoint->SetConnecting(true); endpoint->SetConnecting(true);
/* Use dynamic thread pool with additional on demand resources with fast throughput. */ AddConnection(endpoint);
EnqueueAsyncCallback(std::bind(&ApiListener::AddConnection, this, endpoint), LowLatencyScheduler);
} }
} }
@ -1198,8 +1259,7 @@ void ApiListener::ReplayLog(const JsonRpcConnection::Ptr& client)
} }
try { try {
size_t bytesSent = NetString::WriteStringToStream(client->GetStream(), pmessage->Get("message")); client->SendRawMessage(pmessage->Get("message"));
endpoint->AddMessageSent(bytesSent);
count++; count++;
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
Log(LogWarning, "ApiListener") Log(LogWarning, "ApiListener")
@ -1224,8 +1284,7 @@ void ApiListener::ReplayLog(const JsonRpcConnection::Ptr& client)
}) } }) }
}); });
size_t bytesSent = JsonRpc::SendMessage(client->GetStream(), lmessage); client->SendMessage(lmessage);
endpoint->AddMessageSent(bytesSent);
} }
} }
@ -1344,8 +1403,6 @@ std::pair<Dictionary::Ptr, Dictionary::Ptr> ApiListener::GetStatus()
/* connection stats */ /* connection stats */
size_t jsonRpcAnonymousClients = GetAnonymousClients().size(); size_t jsonRpcAnonymousClients = GetAnonymousClients().size();
size_t httpClients = GetHttpClients().size(); size_t httpClients = GetHttpClients().size();
size_t workQueueItems = JsonRpcConnection::GetWorkQueueLength();
size_t workQueueCount = JsonRpcConnection::GetWorkQueueCount();
size_t syncQueueItems = m_SyncQueue.GetLength(); size_t syncQueueItems = m_SyncQueue.GetLength();
size_t relayQueueItems = m_RelayQueue.GetLength(); size_t relayQueueItems = m_RelayQueue.GetLength();
double workQueueItemRate = JsonRpcConnection::GetWorkQueueRate(); double workQueueItemRate = JsonRpcConnection::GetWorkQueueRate();
@ -1364,8 +1421,6 @@ std::pair<Dictionary::Ptr, Dictionary::Ptr> ApiListener::GetStatus()
{ "json_rpc", new Dictionary({ { "json_rpc", new Dictionary({
{ "anonymous_clients", jsonRpcAnonymousClients }, { "anonymous_clients", jsonRpcAnonymousClients },
{ "work_queue_items", workQueueItems },
{ "work_queue_count", workQueueCount },
{ "sync_queue_items", syncQueueItems }, { "sync_queue_items", syncQueueItems },
{ "relay_queue_items", relayQueueItems }, { "relay_queue_items", relayQueueItems },
{ "work_queue_item_rate", workQueueItemRate }, { "work_queue_item_rate", workQueueItemRate },
@ -1385,8 +1440,6 @@ std::pair<Dictionary::Ptr, Dictionary::Ptr> ApiListener::GetStatus()
perfdata->Set("num_json_rpc_anonymous_clients", jsonRpcAnonymousClients); perfdata->Set("num_json_rpc_anonymous_clients", jsonRpcAnonymousClients);
perfdata->Set("num_http_clients", httpClients); 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_sync_queue_items", syncQueueItems);
perfdata->Set("num_json_rpc_relay_queue_items", relayQueueItems); perfdata->Set("num_json_rpc_relay_queue_items", relayQueueItems);
@ -1513,14 +1566,13 @@ String ApiListener::GetFromZoneName(const Zone::Ptr& fromZone)
return fromZoneName; return fromZoneName;
} }
void ApiListener::UpdateStatusFile(TcpSocket::Ptr socket) void ApiListener::UpdateStatusFile(boost::asio::ip::tcp::endpoint localEndpoint)
{ {
String path = Configuration::CacheDir + "/api-state.json"; String path = Configuration::CacheDir + "/api-state.json";
std::pair<String, String> details = socket->GetClientAddressDetails();
Utility::SaveJsonFile(path, 0644, new Dictionary({ Utility::SaveJsonFile(path, 0644, new Dictionary({
{"host", details.first}, {"host", String(localEndpoint.address().to_string())},
{"port", Convert::ToLong(details.second)} {"port", localEndpoint.port()}
})); }));
} }

View File

@ -14,6 +14,9 @@
#include "base/tcpsocket.hpp" #include "base/tcpsocket.hpp"
#include "base/tlsstream.hpp" #include "base/tlsstream.hpp"
#include "base/threadpool.hpp" #include "base/threadpool.hpp"
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <set> #include <set>
namespace icinga namespace icinga
@ -105,8 +108,7 @@ protected:
void ValidateTlsHandshakeTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) override; void ValidateTlsHandshakeTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) override;
private: private:
std::shared_ptr<SSL_CTX> m_SSLContext; std::shared_ptr<boost::asio::ssl::context> m_SSLContext;
std::set<TcpSocket::Ptr> m_Servers;
mutable boost::mutex m_AnonymousClientsLock; mutable boost::mutex m_AnonymousClientsLock;
mutable boost::mutex m_HttpClientsLock; mutable boost::mutex m_HttpClientsLock;
@ -128,12 +130,9 @@ private:
bool AddListener(const String& node, const String& service); bool AddListener(const String& node, const String& service);
void AddConnection(const Endpoint::Ptr& endpoint); void AddConnection(const Endpoint::Ptr& endpoint);
void NewClientHandler(const Socket::Ptr& client, const String& hostname, ConnectionRole role); void NewClientHandler(boost::asio::yield_context yc, const std::shared_ptr<AsioTlsStream>& client, const String& hostname, ConnectionRole role);
void NewClientHandlerInternal(const Socket::Ptr& client, const String& hostname, ConnectionRole role); void NewClientHandlerInternal(boost::asio::yield_context yc, const std::shared_ptr<AsioTlsStream>& client, const String& hostname, ConnectionRole role);
void ListenerThreadProc(const Socket::Ptr& server); void ListenerCoroutineProc(boost::asio::yield_context yc, const std::shared_ptr<boost::asio::ip::tcp::acceptor>& server, const std::shared_ptr<boost::asio::ssl::context>& sslContext);
static ThreadPool& GetTP();
static void EnqueueAsyncCallback(const std::function<void ()>& callback, SchedulerPolicy policy = DefaultScheduler);
WorkQueue m_RelayQueue; WorkQueue m_RelayQueue;
WorkQueue m_SyncQueue{0, 4}; WorkQueue m_SyncQueue{0, 4};
@ -154,7 +153,7 @@ private:
static void CopyCertificateFile(const String& oldCertPath, const String& newCertPath); static void CopyCertificateFile(const String& oldCertPath, const String& newCertPath);
void UpdateStatusFile(TcpSocket::Ptr socket); void UpdateStatusFile(boost::asio::ip::tcp::endpoint localEndpoint);
void RemoveStatusFile(); void RemoveStatusFile();
/* filesync */ /* filesync */

View File

@ -12,12 +12,23 @@ using namespace icinga;
REGISTER_URLHANDLER("/v1/config/files", ConfigFilesHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestMethod != "GET") namespace http = boost::beast::http;
if (request.method() != http::verb::get)
return false; return false;
const std::vector<String>& urlPath = request.RequestUrl->GetPath(); const std::vector<String>& urlPath = url->GetPath();
if (urlPath.size() >= 4) if (urlPath.size() >= 4)
params->Set("package", urlPath[3]); 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, "/")); 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'."); HttpUtility::SendJsonError(response, params, 400, "Invalid Accept header. Either remove the Accept header or set it to 'application/octet-stream'.");
return true; return true;
} }
@ -69,9 +80,10 @@ bool ConfigFilesHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& re
fp.exceptions(std::ifstream::badbit); fp.exceptions(std::ifstream::badbit);
String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>()); String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
response.SetStatus(200, "OK"); response.result(http::status::ok);
response.AddHeader("Content-Type", "application/octet-stream"); response.set(http::field::content_type, "application/octet-stream");
response.WriteBody(content.CStr(), content.GetLength()); response.body() = content;
response.set(http::field::content_length, response.body().size());
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 500, "Could not read file.", HttpUtility::SendJsonError(response, params, 500, "Could not read file.",
DiagnosticInformation(ex)); DiagnosticInformation(ex));

View File

@ -13,8 +13,16 @@ class ConfigFilesHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(ConfigFilesHandler); DECLARE_PTR_TYPEDEFS(ConfigFilesHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -10,25 +10,44 @@ using namespace icinga;
REGISTER_URLHANDLER("/v1/config/packages", ConfigPackagesHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() > 4) namespace http = boost::beast::http;
if (url->GetPath().size() > 4)
return false; return false;
if (request.RequestMethod == "GET") if (request.method() == http::verb::get)
HandleGet(user, request, response, params); HandleGet(user, request, url, response, params);
else if (request.RequestMethod == "POST") else if (request.method() == http::verb::post)
HandlePost(user, request, response, params); HandlePost(user, request, url, response, params);
else if (request.RequestMethod == "DELETE") else if (request.method() == http::verb::delete_)
HandleDelete(user, request, response, params); HandleDelete(user, request, url, response, params);
else else
return false; return false;
return true; 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
)
{ {
namespace http = boost::beast::http;
FilterUtility::CheckPermission(user, "config/query"); FilterUtility::CheckPermission(user, "config/query");
std::vector<String> packages; std::vector<String> packages;
@ -58,16 +77,24 @@ void ConfigPackagesHandler::HandleGet(const ApiUser::Ptr& user, HttpRequest& req
{ "results", new Array(std::move(results)) } { "results", new Array(std::move(results)) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
)
{ {
namespace http = boost::beast::http;
FilterUtility::CheckPermission(user, "config/modify"); FilterUtility::CheckPermission(user, "config/modify");
if (request.RequestUrl->GetPath().size() >= 4) if (url->GetPath().size() >= 4)
params->Set("package", request.RequestUrl->GetPath()[3]); params->Set("package", url->GetPath()[3]);
String packageName = HttpUtility::GetLastParameter(params, "package"); String packageName = HttpUtility::GetLastParameter(params, "package");
@ -95,16 +122,24 @@ void ConfigPackagesHandler::HandlePost(const ApiUser::Ptr& user, HttpRequest& re
{ "results", new Array({ result1 }) } { "results", new Array({ result1 }) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
)
{ {
namespace http = boost::beast::http;
FilterUtility::CheckPermission(user, "config/modify"); FilterUtility::CheckPermission(user, "config/modify");
if (request.RequestUrl->GetPath().size() >= 4) if (url->GetPath().size() >= 4)
params->Set("package", request.RequestUrl->GetPath()[3]); params->Set("package", url->GetPath()[3]);
String packageName = HttpUtility::GetLastParameter(params, "package"); String packageName = HttpUtility::GetLastParameter(params, "package");
@ -131,6 +166,6 @@ void ConfigPackagesHandler::HandleDelete(const ApiUser::Ptr& user, HttpRequest&
{ "results", new Array({ result1 }) } { "results", new Array({ result1 }) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
} }

View File

@ -13,16 +13,39 @@ class ConfigPackagesHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(ConfigPackagesHandler); DECLARE_PTR_TYPEDEFS(ConfigPackagesHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
private: private:
void HandleGet(const ApiUser::Ptr& user, HttpRequest& request, void HandleGet(
HttpResponse& response, const Dictionary::Ptr& params); const ApiUser::Ptr& user,
void HandlePost(const ApiUser::Ptr& user, HttpRequest& request, boost::beast::http::request<boost::beast::http::string_body>& request,
HttpResponse& response, const Dictionary::Ptr& params); const Url::Ptr& url,
void HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, boost::beast::http::response<boost::beast::http::string_body>& response,
HttpResponse& response, const Dictionary::Ptr& params); const Dictionary::Ptr& params
);
void HandlePost(
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
);
void HandleDelete(
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
);
}; };

View File

@ -11,32 +11,51 @@ using namespace icinga;
REGISTER_URLHANDLER("/v1/config/stages", ConfigStagesHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() > 5) namespace http = boost::beast::http;
if (url->GetPath().size() > 5)
return false; return false;
if (request.RequestMethod == "GET") if (request.method() == http::verb::get)
HandleGet(user, request, response, params); HandleGet(user, request, url, response, params);
else if (request.RequestMethod == "POST") else if (request.method() == http::verb::post)
HandlePost(user, request, response, params); HandlePost(user, request, url, response, params);
else if (request.RequestMethod == "DELETE") else if (request.method() == http::verb::delete_)
HandleDelete(user, request, response, params); HandleDelete(user, request, url, response, params);
else else
return false; return false;
return true; 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
)
{ {
namespace http = boost::beast::http;
FilterUtility::CheckPermission(user, "config/query"); FilterUtility::CheckPermission(user, "config/query");
if (request.RequestUrl->GetPath().size() >= 4) if (url->GetPath().size() >= 4)
params->Set("package", request.RequestUrl->GetPath()[3]); params->Set("package", url->GetPath()[3]);
if (request.RequestUrl->GetPath().size() >= 5) if (url->GetPath().size() >= 5)
params->Set("stage", request.RequestUrl->GetPath()[4]); params->Set("stage", url->GetPath()[4]);
String packageName = HttpUtility::GetLastParameter(params, "package"); String packageName = HttpUtility::GetLastParameter(params, "package");
String stageName = HttpUtility::GetLastParameter(params, "stage"); 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)) } { "results", new Array(std::move(results)) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
)
{ {
namespace http = boost::beast::http;
FilterUtility::CheckPermission(user, "config/modify"); FilterUtility::CheckPermission(user, "config/modify");
if (request.RequestUrl->GetPath().size() >= 4) if (url->GetPath().size() >= 4)
params->Set("package", request.RequestUrl->GetPath()[3]); params->Set("package", url->GetPath()[3]);
String packageName = HttpUtility::GetLastParameter(params, "package"); String packageName = HttpUtility::GetLastParameter(params, "package");
@ -123,19 +150,27 @@ void ConfigStagesHandler::HandlePost(const ApiUser::Ptr& user, HttpRequest& requ
{ "results", new Array({ result1 }) } { "results", new Array({ result1 }) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
)
{ {
namespace http = boost::beast::http;
FilterUtility::CheckPermission(user, "config/modify"); FilterUtility::CheckPermission(user, "config/modify");
if (request.RequestUrl->GetPath().size() >= 4) if (url->GetPath().size() >= 4)
params->Set("package", request.RequestUrl->GetPath()[3]); params->Set("package", url->GetPath()[3]);
if (request.RequestUrl->GetPath().size() >= 5) if (url->GetPath().size() >= 5)
params->Set("stage", request.RequestUrl->GetPath()[4]); params->Set("stage", url->GetPath()[4]);
String packageName = HttpUtility::GetLastParameter(params, "package"); String packageName = HttpUtility::GetLastParameter(params, "package");
String stageName = HttpUtility::GetLastParameter(params, "stage"); String stageName = HttpUtility::GetLastParameter(params, "stage");
@ -165,7 +200,7 @@ void ConfigStagesHandler::HandleDelete(const ApiUser::Ptr& user, HttpRequest& re
{ "results", new Array({ result1 }) } { "results", new Array({ result1 }) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
} }

View File

@ -13,16 +13,39 @@ class ConfigStagesHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(ConfigStagesHandler); DECLARE_PTR_TYPEDEFS(ConfigStagesHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
private: private:
void HandleGet(const ApiUser::Ptr& user, HttpRequest& request, void HandleGet(
HttpResponse& response, const Dictionary::Ptr& params); const ApiUser::Ptr& user,
void HandlePost(const ApiUser::Ptr& user, HttpRequest& request, boost::beast::http::request<boost::beast::http::string_body>& request,
HttpResponse& response, const Dictionary::Ptr& params); const Url::Ptr& url,
void HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, boost::beast::http::response<boost::beast::http::string_body>& response,
HttpResponse& response, const Dictionary::Ptr& params); const Dictionary::Ptr& params
);
void HandlePost(
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
);
void HandleDelete(
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params
);
}; };

View File

@ -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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() != 3) namespace http = boost::beast::http;
if (url->GetPath().size() != 3)
return false; return false;
if (request.RequestMethod != "POST") if (request.method() != http::verb::post)
return false; return false;
QueryDescription qd; QueryDescription qd;
String methodName = request.RequestUrl->GetPath()[2]; String methodName = url->GetPath()[2];
FilterUtility::CheckPermission(user, "console"); FilterUtility::CheckPermission(user, "console");
@ -85,9 +96,12 @@ bool ConsoleHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& reques
return true; return true;
} }
bool ConsoleHandler::ExecuteScriptHelper(HttpRequest& request, HttpResponse& response, bool ConsoleHandler::ExecuteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed) const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
{ {
namespace http = boost::beast::http;
Log(LogNotice, "Console") Log(LogNotice, "Console")
<< "Executing expression: " << command; << "Executing expression: " << command;
@ -151,15 +165,18 @@ bool ConsoleHandler::ExecuteScriptHelper(HttpRequest& request, HttpResponse& res
{ "results", new Array({ resultInfo }) } { "results", new Array({ resultInfo }) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
return true; return true;
} }
bool ConsoleHandler::AutocompleteScriptHelper(HttpRequest& request, HttpResponse& response, bool ConsoleHandler::AutocompleteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed) const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
{ {
namespace http = boost::beast::http;
Log(LogInformation, "Console") Log(LogInformation, "Console")
<< "Auto-completing expression: " << command; << "Auto-completing expression: " << command;
@ -187,7 +204,7 @@ bool ConsoleHandler::AutocompleteScriptHelper(HttpRequest& request, HttpResponse
{ "results", new Array({ result1 }) } { "results", new Array({ result1 }) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
return true; return true;

View File

@ -22,15 +22,25 @@ class ConsoleHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(ConsoleHandler); DECLARE_PTR_TYPEDEFS(ConsoleHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
static std::vector<String> GetAutocompletionSuggestions(const String& word, ScriptFrame& frame); static std::vector<String> GetAutocompletionSuggestions(const String& word, ScriptFrame& frame);
private: private:
static bool ExecuteScriptHelper(HttpRequest& request, HttpResponse& response, static bool ExecuteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed); 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<boost::beast::http::string_body>& request,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed); const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed);
}; };

View File

@ -14,15 +14,26 @@ using namespace icinga;
REGISTER_URLHANDLER("/v1/objects", CreateObjectHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() != 4) namespace http = boost::beast::http;
if (url->GetPath().size() != 4)
return false; return false;
if (request.RequestMethod != "PUT") if (request.method() != http::verb::put)
return false; return false;
Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
if (!type) { if (!type) {
HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); 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()); FilterUtility::CheckPermission(user, "objects/create/" + type->GetName());
String name = request.RequestUrl->GetPath()[3]; String name = url->GetPath()[3];
Array::Ptr templates = params->Get("templates"); Array::Ptr templates = params->Get("templates");
Dictionary::Ptr attrs = params->Get("attrs"); 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("code", 500);
result1->Set("status", "Object could not be created."); 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); HttpUtility::SendJsonBody(response, params, result);
return true; return true;
@ -113,7 +124,7 @@ bool CreateObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r
if (verbose) if (verbose)
result1->Set("diagnostic_information", diagnosticInformation); 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); HttpUtility::SendJsonBody(response, params, result);
return true; return true;
@ -129,7 +140,7 @@ bool CreateObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r
else if (!obj && ignoreOnError) else if (!obj && ignoreOnError)
result1->Set("status", "Object was not created but 'ignore_on_error' was set to true"); 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); HttpUtility::SendJsonBody(response, params, result);
return true; return true;

View File

@ -13,8 +13,16 @@ class CreateObjectHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(CreateObjectHandler); DECLARE_PTR_TYPEDEFS(CreateObjectHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -14,15 +14,26 @@ using namespace icinga;
REGISTER_URLHANDLER("/v1/objects", DeleteObjectHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
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; return false;
if (request.RequestMethod != "DELETE") if (request.method() != http::verb::delete_)
return false; return false;
Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
if (!type) { if (!type) {
HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); 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()); params->Set("type", type->GetName());
if (request.RequestUrl->GetPath().size() >= 4) { if (url->GetPath().size() >= 4) {
String attr = type->GetName(); String attr = type->GetName();
boost::algorithm::to_lower(attr); boost::algorithm::to_lower(attr);
params->Set(attr, request.RequestUrl->GetPath()[3]); params->Set(attr, url->GetPath()[3]);
} }
std::vector<Value> objs; std::vector<Value> objs;
@ -93,9 +104,9 @@ bool DeleteObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r
}); });
if (!success) if (!success)
response.SetStatus(500, "One or more objects could not be deleted"); response.result(http::status::internal_server_error);
else else
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);

View File

@ -13,8 +13,16 @@ class DeleteObjectHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(DeleteObjectHandler); DECLARE_PTR_TYPEDEFS(DeleteObjectHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -2,8 +2,10 @@
#include "remote/eventqueue.hpp" #include "remote/eventqueue.hpp"
#include "remote/filterutility.hpp" #include "remote/filterutility.hpp"
#include "base/io-engine.hpp"
#include "base/singleton.hpp" #include "base/singleton.hpp"
#include "base/logger.hpp" #include "base/logger.hpp"
#include <boost/asio/spawn.hpp>
using namespace icinga; 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::Ptr> EventQueue::GetQueuesForType(const String& type) std::vector<EventQueue::Ptr> EventQueue::GetQueuesForType(const String& type)
{ {
EventQueueRegistry::ItemMap queues = EventQueueRegistry::GetInstance()->GetItems(); EventQueueRegistry::ItemMap queues = EventQueueRegistry::GetInstance()->GetItems();

View File

@ -6,6 +6,7 @@
#include "remote/httphandler.hpp" #include "remote/httphandler.hpp"
#include "base/object.hpp" #include "base/object.hpp"
#include "config/expression.hpp" #include "config/expression.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/thread/mutex.hpp> #include <boost/thread/mutex.hpp>
#include <boost/thread/condition_variable.hpp> #include <boost/thread/condition_variable.hpp>
#include <set> #include <set>
@ -31,6 +32,7 @@ public:
void SetFilter(std::unique_ptr<Expression> filter); void SetFilter(std::unique_ptr<Expression> filter);
Dictionary::Ptr WaitForEvent(void *client, double timeout = 5); Dictionary::Ptr WaitForEvent(void *client, double timeout = 5);
Dictionary::Ptr WaitForEvent(void *client, boost::asio::yield_context yc);
static std::vector<EventQueue::Ptr> GetQueuesForType(const String& type); static std::vector<EventQueue::Ptr> GetQueuesForType(const String& type);
static void UnregisterIfUnused(const String& name, const EventQueue::Ptr& queue); static void UnregisterIfUnused(const String& name, const EventQueue::Ptr& queue);

View File

@ -5,23 +5,39 @@
#include "remote/filterutility.hpp" #include "remote/filterutility.hpp"
#include "config/configcompiler.hpp" #include "config/configcompiler.hpp"
#include "config/expression.hpp" #include "config/expression.hpp"
#include "base/defer.hpp"
#include "base/io-engine.hpp"
#include "base/objectlock.hpp" #include "base/objectlock.hpp"
#include "base/json.hpp" #include "base/json.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/write.hpp>
#include <boost/algorithm/string/replace.hpp> #include <boost/algorithm/string/replace.hpp>
using namespace icinga; using namespace icinga;
REGISTER_URLHANDLER("/v1/events", EventsHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() != 2) namespace asio = boost::asio;
namespace http = boost::beast::http;
if (url->GetPath().size() != 2)
return false; return false;
if (request.RequestMethod != "POST") if (request.method() != http::verb::post)
return false; return false;
if (request.ProtocolVersion == HttpVersion10) { if (request.version() == 10) {
HttpUtility::SendJsonError(response, params, 400, "HTTP/1.0 not supported for event streams."); HttpUtility::SendJsonError(response, params, 400, "HTTP/1.0 not supported for event streams.");
return true; return true;
} }
@ -67,33 +83,37 @@ bool EventsHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request
queue->AddClient(&request); queue->AddClient(&request);
response.SetStatus(200, "OK"); Defer removeClient ([&queue, &request, &queueName]() {
response.AddHeader("Content-Type", "application/json");
for (;;) {
Dictionary::Ptr result = queue->WaitForEvent(&request);
if (!response.IsPeerConnected()) {
queue->RemoveClient(&request); queue->RemoveClient(&request);
EventQueue::UnregisterIfUnused(queueName, queue); EventQueue::UnregisterIfUnused(queueName, queue);
return true; });
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);
} }
if (!result) asio::const_buffer newLine ("\n", 1);
continue;
String body = JsonEncode(result); for (;;) {
String body = JsonEncode(queue->WaitForEvent(&request, yc));
boost::algorithm::replace_all(body, "\n", ""); boost::algorithm::replace_all(body, "\n", "");
try { asio::const_buffer payload (body.CStr(), body.GetLength());
response.WriteBody(body.CStr(), body.GetLength());
response.WriteBody("\n", 1); IoBoundWorkSlot dontLockTheIoThreadWhileWriting (yc);
} catch (const std::exception&) {
queue->RemoveClient(&request); asio::async_write(stream, payload, yc);
EventQueue::UnregisterIfUnused(queueName, queue); asio::async_write(stream, newLine, yc);
throw; stream.async_flush(yc);
}
} }
} }

View File

@ -14,8 +14,16 @@ class EventsHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(EventsHandler); DECLARE_PTR_TYPEDEFS(EventsHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -5,6 +5,7 @@
#include "base/singleton.hpp" #include "base/singleton.hpp"
#include "base/exception.hpp" #include "base/exception.hpp"
#include <boost/algorithm/string/join.hpp> #include <boost/algorithm/string/join.hpp>
#include <boost/beast/http.hpp>
using namespace icinga; using namespace icinga;
@ -44,11 +45,20 @@ void HttpHandler::Register(const Url::Ptr& url, const HttpHandler::Ptr& handler)
handlers->Add(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<boost::beast::http::string_body>& request,
boost::beast::http::response<boost::beast::http::string_body>& response,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
Dictionary::Ptr node = m_UrlTree; Dictionary::Ptr node = m_UrlTree;
std::vector<HttpHandler::Ptr> handlers; std::vector<HttpHandler::Ptr> handlers;
const std::vector<String>& path = request.RequestUrl->GetPath();
Url::Ptr url = new Url(request.target().to_string());
auto& path (url->GetPath());
for (std::vector<String>::size_type i = 0; i <= path.size(); i++) { for (std::vector<String>::size_type i = 0; i <= path.size(); i++) {
Array::Ptr current_handlers = node->Get("handlers"); Array::Ptr current_handlers = node->Get("handlers");
@ -81,7 +91,7 @@ void HttpHandler::ProcessRequest(const ApiUser::Ptr& user, HttpRequest& request,
Dictionary::Ptr params; Dictionary::Ptr params;
try { try {
params = HttpUtility::FetchRequestParameters(request); params = HttpUtility::FetchRequestParameters(url, request.body());
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 400, "Invalid request body: " + DiagnosticInformation(ex, false)); HttpUtility::SendJsonError(response, params, 400, "Invalid request body: " + DiagnosticInformation(ex, false));
return; return;
@ -89,15 +99,14 @@ void HttpHandler::ProcessRequest(const ApiUser::Ptr& user, HttpRequest& request,
bool processed = false; bool processed = false;
for (const HttpHandler::Ptr& handler : handlers) { 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; processed = true;
break; break;
} }
} }
if (!processed) { if (!processed) {
String path = boost::algorithm::join(request.RequestUrl->GetPath(), "/"); HttpUtility::SendJsonError(response, params, 404, "The requested path '" + boost::algorithm::join(path, "/") +
HttpUtility::SendJsonError(response, params, 404, "The requested path '" + path +
"' could not be found or the request method is not valid for this path."); "' could not be found or the request method is not valid for this path.");
return; return;
} }

View File

@ -4,10 +4,14 @@
#define HTTPHANDLER_H #define HTTPHANDLER_H
#include "remote/i2-remote.hpp" #include "remote/i2-remote.hpp"
#include "remote/url.hpp"
#include "remote/httpresponse.hpp" #include "remote/httpresponse.hpp"
#include "remote/apiuser.hpp" #include "remote/apiuser.hpp"
#include "base/registry.hpp" #include "base/registry.hpp"
#include "base/tlsstream.hpp"
#include <vector> #include <vector>
#include <boost/asio/spawn.hpp>
#include <boost/beast/http.hpp>
namespace icinga namespace icinga
{ {
@ -22,10 +26,26 @@ class HttpHandler : public Object
public: public:
DECLARE_PTR_TYPEDEFS(HttpHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) = 0;
static void Register(const Url::Ptr& url, const HttpHandler::Ptr& handler); 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<boost::beast::http::string_body>& request,
boost::beast::http::response<boost::beast::http::string_body>& response,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
);
private: private:
static Dictionary::Ptr m_UrlTree; static Dictionary::Ptr m_UrlTree;

View File

@ -6,306 +6,144 @@
#include "remote/apilistener.hpp" #include "remote/apilistener.hpp"
#include "remote/apifunction.hpp" #include "remote/apifunction.hpp"
#include "remote/jsonrpc.hpp" #include "remote/jsonrpc.hpp"
#include "base/application.hpp"
#include "base/base64.hpp" #include "base/base64.hpp"
#include "base/convert.hpp" #include "base/convert.hpp"
#include "base/configtype.hpp" #include "base/configtype.hpp"
#include "base/defer.hpp"
#include "base/exception.hpp" #include "base/exception.hpp"
#include "base/logger.hpp" #include "base/logger.hpp"
#include "base/objectlock.hpp" #include "base/objectlock.hpp"
#include "base/timer.hpp" #include "base/timer.hpp"
#include "base/tlsstream.hpp"
#include "base/utility.hpp" #include "base/utility.hpp"
#include <limits>
#include <memory>
#include <stdexcept>
#include <boost/asio/spawn.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/system/system_error.hpp>
#include <boost/thread/once.hpp> #include <boost/thread/once.hpp>
using namespace icinga; using namespace icinga;
static boost::once_flag l_HttpServerConnectionOnceFlag = BOOST_ONCE_INIT; auto const l_ServerHeader ("Icinga/" + Application::GetAppVersion());
static Timer::Ptr l_HttpServerConnectionTimeoutTimer;
HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream) HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const std::shared_ptr<AsioTlsStream>& stream)
: m_Stream(stream), m_Seen(Utility::GetTime()), m_CurrentRequest(stream), m_PendingRequests(0) : m_Stream(stream), m_Seen(Utility::GetTime()), m_IoStrand(stream->get_io_service()), m_ShuttingDown(false)
{ {
boost::call_once(l_HttpServerConnectionOnceFlag, &HttpServerConnection::StaticInitialize); if (authenticated) {
m_RequestQueue.SetName("HttpServerConnection");
if (authenticated)
m_ApiUser = ApiUser::GetByClientCN(identity); m_ApiUser = ApiUser::GetByClientCN(identity);
/* Cache the peer address. */
m_PeerAddress = "<unknown>";
if (stream) {
Socket::Ptr socket = m_Stream->GetSocket();
if (socket) {
m_PeerAddress = socket->GetPeerAddress();
}
}
} }
void HttpServerConnection::StaticInitialize()
{ {
l_HttpServerConnectionTimeoutTimer = new Timer(); std::ostringstream address;
l_HttpServerConnectionTimeoutTimer->OnTimerExpired.connect(std::bind(&HttpServerConnection::TimeoutTimerHandler)); auto endpoint (stream->lowest_layer().remote_endpoint());
l_HttpServerConnectionTimeoutTimer->SetInterval(5);
l_HttpServerConnectionTimeoutTimer->Start(); address << '[' << endpoint.address() << "]:" << endpoint.port();
m_PeerAddress = address.str();
}
} }
void HttpServerConnection::Start() void HttpServerConnection::Start()
{ {
/* the stream holds an owning reference to this object through the callback we're registering here */ namespace asio = boost::asio;
m_Stream->RegisterDataHandler(std::bind(&HttpServerConnection::DataAvailableHandler, HttpServerConnection::Ptr(this)));
if (m_Stream->IsDataAvailable())
DataAvailableHandler();
}
ApiUser::Ptr HttpServerConnection::GetApiUser() const HttpServerConnection::Ptr keepAlive (this);
{
return m_ApiUser;
}
TlsStream::Ptr HttpServerConnection::GetStream() const asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { ProcessMessages(yc); });
{ asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) { CheckLiveness(yc); });
return m_Stream;
} }
void HttpServerConnection::Disconnect() void HttpServerConnection::Disconnect()
{ {
boost::recursive_mutex::scoped_try_lock lock(m_DataHandlerMutex); namespace asio = boost::asio;
if (!lock.owns_lock()) {
Log(LogInformation, "HttpServerConnection", "Unable to disconnect Http client, I/O thread busy"); HttpServerConnection::Ptr keepAlive (this);
return;
} asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
if (!m_ShuttingDown) {
m_ShuttingDown = true;
Log(LogInformation, "HttpServerConnection") Log(LogInformation, "HttpServerConnection")
<< "HTTP client disconnected (from " << m_PeerAddress << ")"; << "HTTP client disconnected (from " << m_PeerAddress << ")";
ApiListener::Ptr listener = ApiListener::GetInstance(); try {
m_Stream->next_layer().async_shutdown(yc);
} catch (...) {
}
try {
m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both);
} catch (...) {
}
auto listener (ApiListener::GetInstance());
if (listener) {
CpuBoundWork removeHttpClient (yc);
listener->RemoveHttpClient(this); listener->RemoveHttpClient(this);
m_CurrentRequest.~HttpRequest();
new (&m_CurrentRequest) HttpRequest(nullptr);
m_Stream->Close();
}
bool HttpServerConnection::ProcessMessage()
{
bool res;
HttpResponse response(m_Stream, m_CurrentRequest);
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("<h1>Bad Request</h1><p><pre>") + ex.what() + "</pre></p>";
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 = "<h1>Internal Server Error</h1><p><pre>" + DiagnosticInformation(ex) + "</pre></p>";
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("<h1>Bad Request</h1><p><pre>") + ex.what() + "</pre></p>";
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 = "<h1>Internal Server Error</h1><p><pre>" + DiagnosticInformation(ex) + "</pre></p>";
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() : "<unauthenticated>") << ")";
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);
}
}
response.AddHeader("Access-Control-Allow-Credentials", "true");
String accessControlRequestMethodHeader = m_CurrentRequest.Headers->Get("access-control-request-method");
if (m_CurrentRequest.RequestMethod == "OPTIONS" && !accessControlRequestMethodHeader.IsEmpty()) {
response.SetStatus(200, "OK");
response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
response.AddHeader("Access-Control-Allow-Headers", "Authorization, X-HTTP-Method-Override");
String msg = "Preflight OK";
response.WriteBody(msg.CStr(), msg.GetLength());
response.Finish();
return false;
}
}
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 = "<h1>Accept header is missing or not set to 'application/json'.</h1>";
response.WriteBody(msg.CStr(), msg.GetLength());
response.Finish();
return false;
}
if (!m_AuthenticatedUser) {
Log(LogWarning, "HttpServerConnection")
<< "Unauthorized request: " << m_CurrentRequest.RequestMethod << " " << requestUrl;
response.SetStatus(401, "Unauthorized");
response.AddHeader("WWW-Authenticate", "Basic realm=\"Icinga 2\"");
if (m_CurrentRequest.Headers->Get("accept") == "application/json") {
Dictionary::Ptr result = new Dictionary({
{ "error", 401 },
{ "status", "Unauthorized. Please check your user credentials." }
}); });
}
HttpUtility::SendJsonBody(response, nullptr, result); static inline
bool EnsureValidHeaders(
AsioTlsStream& stream,
boost::beast::flat_buffer& buf,
boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
boost::beast::http::response<boost::beast::http::string_body>& response,
boost::asio::yield_context& yc
)
{
namespace http = boost::beast::http;
bool httpError = true;
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:
*
* <https://github.com/boostorg/beast/issues/643>
*/
throw std::invalid_argument(ex.what());
}
httpError = false;
switch (parser.get().version()) {
case 10:
case 11:
break;
default:
throw std::invalid_argument("Unsupported HTTP version");
}
} catch (const std::invalid_argument& ex) {
response.result(http::status::bad_request);
if (!httpError && parser.get()[http::field::accept] == "application/json") {
HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
{ "error", 400 },
{ "status", String("Bad Request: ") + ex.what() }
}));
} else { } else {
response.AddHeader("Content-Type", "text/html"); response.set(http::field::content_type, "text/html");
String msg = "<h1>Unauthorized. Please check your user credentials.</h1>"; response.body() = String("<h1>Bad Request</h1><p><pre>") + ex.what() + "</pre></p>";
response.WriteBody(msg.CStr(), msg.GetLength()); response.set(http::field::content_length, response.body().size());
} }
response.Finish(); response.set(http::field::connection, "close");
return false;
}
static const size_t defaultContentLengthLimit = 1 * 1024 * 1024; http::async_write(stream, response, yc);
size_t maxSize = defaultContentLengthLimit; stream.async_flush(yc);
Array::Ptr permissions = m_AuthenticatedUser->GetPermissions();
if (permissions) {
ObjectLock olock(permissions);
for (const Value& permissionInfo : permissions) {
String permission;
if (permissionInfo.IsObjectType<Dictionary>())
permission = static_cast<Dictionary::Ptr>(permissionInfo)->Get("permission");
else
permission = permissionInfo;
static std::vector<std::pair<String, size_t>> specialContentLengthLimits {
{ "config/modify", 512 * 1024 * 1024 }
};
for (const auto& limitInfo : specialContentLengthLimits) {
if (limitInfo.second <= maxSize)
continue;
if (Utility::Match(permission, limitInfo.first))
maxSize = limitInfo.second;
}
}
}
size_t contentLength = m_CurrentRequest.Headers->Get("content-length");
if (contentLength > maxSize) {
response.SetStatus(400, "Bad Request");
String msg = String("<h1>Content length exceeded maximum</h1>");
response.WriteBody(msg.CStr(), msg.GetLength());
response.Finish();
return false; return false;
} }
@ -313,64 +151,370 @@ bool HttpServerConnection::ManageHeaders(HttpResponse& response)
return true; return true;
} }
void HttpServerConnection::ProcessMessageAsync(HttpRequest& request, HttpResponse& response, const ApiUser::Ptr& user) static inline
void HandleExpect100(
AsioTlsStream& stream,
boost::beast::http::request<boost::beast::http::string_body>& request,
boost::asio::yield_context& yc
)
{ {
response.RebindRequest(request); namespace http = boost::beast::http;
try { if (request[http::field::expect] == "100-continue") {
HttpHandler::ProcessRequest(user, request, response); http::response<http::string_body> response;
} catch (const std::exception& ex) {
Log(LogCritical, "HttpServerConnection") response.result(http::status::continue_);
<< "Unhandled exception while processing Http request: " << DiagnosticInformation(ex);
HttpUtility::SendJsonError(response, nullptr, 503, "Unhandled exception" , DiagnosticInformation(ex)); http::async_write(stream, response, yc);
stream.async_flush(yc);
}
} }
response.Finish(); static inline
m_PendingRequests--; bool HandleAccessControl(
} AsioTlsStream& stream,
boost::beast::http::request<boost::beast::http::string_body>& request,
void HttpServerConnection::DataAvailableHandler() boost::beast::http::response<boost::beast::http::string_body>& response,
boost::asio::yield_context& yc
)
{ {
bool close = false; namespace http = boost::beast::http;
if (!m_Stream->IsEof()) { auto listener (ApiListener::GetInstance());
boost::recursive_mutex::scoped_try_lock lock(m_DataHandlerMutex);
if (!lock.owns_lock()) { if (listener) {
Log(LogNotice, "HttpServerConnection", "Unable to process available data, they're already being processed in another thread"); auto headerAllowOrigin (listener->GetAccessControlAllowOrigin());
return;
if (headerAllowOrigin) {
CpuBoundWork allowOriginHeader (yc);
auto allowedOrigins (headerAllowOrigin->ToSet<String>());
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);
} }
try { allowOriginHeader.Done();
while (ProcessMessage())
; /* empty loop body */ response.set(http::field::access_control_allow_credentials, "true");
} catch (const std::exception& ex) {
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<boost::beast::http::string_body>& request,
boost::beast::http::response<boost::beast::http::string_body>& response,
boost::asio::yield_context& yc
)
{
namespace http = boost::beast::http;
if (request.method() != http::verb::get && request[http::field::accept] != "application/json") {
response.result(http::status::bad_request);
response.set(http::field::content_type, "text/html");
response.body() = "<h1>Accept header is missing or not set to 'application/json'.</h1>";
response.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<boost::beast::http::string_body>& request,
ApiUser::Ptr& authenticatedUser,
boost::beast::http::response<boost::beast::http::string_body>& response,
boost::asio::yield_context& yc
)
{
namespace http = boost::beast::http;
if (!authenticatedUser) {
Log(LogWarning, "HttpServerConnection") Log(LogWarning, "HttpServerConnection")
<< "Error while reading Http request: " << DiagnosticInformation(ex); << "Unauthorized request: " << request.method_string() << ' ' << request.target();
close = true; response.result(http::status::unauthorized);
} response.set(http::field::www_authenticate, "Basic realm=\"Icinga 2\"");
} else response.set(http::field::connection, "close");
close = true;
if (close) if (request[http::field::accept] == "application/json") {
Disconnect(); HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
{ "error", 401 },
{ "status", "Unauthorized. Please check your user credentials." }
}));
} else {
response.set(http::field::content_type, "text/html");
response.body() = "<h1>Unauthorized. Please check your user credentials.</h1>";
response.set(http::field::content_length, response.body().size());
} }
void HttpServerConnection::CheckLiveness() http::async_write(stream, response, yc);
stream.async_flush(yc);
return false;
}
return true;
}
static inline
bool EnsureValidBody(
AsioTlsStream& stream,
boost::beast::flat_buffer& buf,
boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
ApiUser::Ptr& authenticatedUser,
boost::beast::http::response<boost::beast::http::string_body>& response,
boost::asio::yield_context& yc
)
{ {
if (m_Seen < Utility::GetTime() - 10 && m_PendingRequests == 0 && m_Stream->IsEof()) { namespace http = boost::beast::http;
{
size_t maxSize = 1024 * 1024;
Array::Ptr permissions = authenticatedUser->GetPermissions();
if (permissions) {
CpuBoundWork evalPermissions (yc);
ObjectLock olock(permissions);
for (const Value& permissionInfo : permissions) {
String permission;
if (permissionInfo.IsObjectType<Dictionary>()) {
permission = static_cast<Dictionary::Ptr>(permissionInfo)->Get("permission");
} else {
permission = permissionInfo;
}
static std::vector<std::pair<String, size_t>> specialContentLengthLimits {
{ "config/modify", 512 * 1024 * 1024 }
};
for (const auto& limitInfo : specialContentLengthLimits) {
if (limitInfo.second <= maxSize) {
continue;
}
if (Utility::Match(permission, limitInfo.first)) {
maxSize = limitInfo.second;
}
}
}
}
parser.body_limit(maxSize);
}
try {
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:
*
* <https://github.com/boostorg/beast/issues/643>
*/
response.result(http::status::bad_request);
if (parser.get()[http::field::accept] == "application/json") {
HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
{ "error", 400 },
{ "status", String("Bad Request: ") + ex.what() }
}));
} else {
response.set(http::field::content_type, "text/html");
response.body() = String("<h1>Bad Request</h1><p><pre>") + ex.what() + "</pre></p>";
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<boost::beast::http::string_body>& request,
ApiUser::Ptr& authenticatedUser,
boost::beast::http::response<boost::beast::http::string_body>& response,
boost::asio::yield_context& yc
)
{
namespace http = boost::beast::http;
bool hasStartedStreaming = false;
try {
CpuBoundWork handlingRequest (yc);
HttpHandler::ProcessRequest(stream, authenticatedUser, request, response, yc, hasStartedStreaming);
} catch (const std::exception& ex) {
if (hasStartedStreaming) {
return false;
}
http::response<http::string_body> response;
HttpUtility::SendJsonError(response, nullptr, 500, "Unhandled exception" , DiagnosticInformation(ex));
http::async_write(stream, response, yc);
stream.async_flush(yc);
return true;
}
if (hasStartedStreaming) {
return false;
}
http::async_write(stream, response, yc);
stream.async_flush(yc);
return true;
}
void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
{
namespace beast = boost::beast;
namespace http = beast::http;
Defer disconnect ([this]() { Disconnect(); });
try {
beast::flat_buffer buf;
for (;;) {
m_Seen = Utility::GetTime();
http::parser<true, http::string_body> parser;
http::response<http::string_body> response;
parser.header_limit(1024 * 1024);
parser.body_limit(-1);
response.set(http::field::server, l_ServerHeader);
if (!EnsureValidHeaders(*m_Stream, buf, parser, response, 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") Log(LogInformation, "HttpServerConnection")
<< "No messages for Http connection have been received in the last 10 seconds."; << "Request: " << request.method_string() << ' ' << request.target()
Disconnect(); << " (from " << m_PeerAddress
<< "), user: " << (authenticatedUser ? authenticatedUser->GetName() : "<unauthenticated>") << ')';
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<decltype(m_Seen)>::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);
}
} }
} }
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()) { for (;;) {
client->CheckLiveness(); 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;
}
}
}

View File

@ -3,12 +3,12 @@
#ifndef HTTPSERVERCONNECTION_H #ifndef HTTPSERVERCONNECTION_H
#define HTTPSERVERCONNECTION_H #define HTTPSERVERCONNECTION_H
#include "remote/httprequest.hpp"
#include "remote/httpresponse.hpp"
#include "remote/apiuser.hpp" #include "remote/apiuser.hpp"
#include "base/string.hpp"
#include "base/tlsstream.hpp" #include "base/tlsstream.hpp"
#include "base/workqueue.hpp" #include <memory>
#include <boost/thread/recursive_mutex.hpp> #include <boost/asio/io_service_strand.hpp>
#include <boost/asio/spawn.hpp>
namespace icinga namespace icinga
{ {
@ -23,39 +23,21 @@ class HttpServerConnection final : public Object
public: public:
DECLARE_PTR_TYPEDEFS(HttpServerConnection); DECLARE_PTR_TYPEDEFS(HttpServerConnection);
HttpServerConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream); HttpServerConnection(const String& identity, bool authenticated, const std::shared_ptr<AsioTlsStream>& stream);
void Start(); void Start();
ApiUser::Ptr GetApiUser() const;
bool IsAuthenticated() const;
TlsStream::Ptr GetStream() const;
void Disconnect(); void Disconnect();
private: private:
ApiUser::Ptr m_ApiUser; ApiUser::Ptr m_ApiUser;
ApiUser::Ptr m_AuthenticatedUser; std::shared_ptr<AsioTlsStream> m_Stream;
TlsStream::Ptr m_Stream;
double m_Seen; double m_Seen;
HttpRequest m_CurrentRequest;
boost::recursive_mutex m_DataHandlerMutex;
WorkQueue m_RequestQueue;
int m_PendingRequests;
String m_PeerAddress; String m_PeerAddress;
boost::asio::io_service::strand m_IoStrand;
bool m_ShuttingDown;
StreamReadContext m_Context; void ProcessMessages(boost::asio::yield_context yc);
void CheckLiveness(boost::asio::yield_context yc);
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&);
}; };
} }

View File

@ -1,27 +1,23 @@
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
#include "remote/httputility.hpp" #include "remote/httputility.hpp"
#include "remote/url.hpp"
#include "base/json.hpp" #include "base/json.hpp"
#include "base/logger.hpp" #include "base/logger.hpp"
#include <map> #include <map>
#include <string>
#include <vector> #include <vector>
#include <boost/beast/http.hpp>
using namespace icinga; using namespace icinga;
Dictionary::Ptr HttpUtility::FetchRequestParameters(HttpRequest& request) Dictionary::Ptr HttpUtility::FetchRequestParameters(const Url::Ptr& url, const std::string& body)
{ {
Dictionary::Ptr result; Dictionary::Ptr result;
String body; if (!body.empty()) {
char buffer[1024];
size_t count;
while ((count = request.ReadBody(buffer, sizeof(buffer))) > 0)
body += String(buffer, buffer + count);
if (!body.IsEmpty()) {
Log(LogDebug, "HttpUtility") Log(LogDebug, "HttpUtility")
<< "Request body: '" << body << "'"; << "Request body: '" << body << '\'';
result = JsonDecode(body); result = JsonDecode(body);
} }
@ -30,7 +26,7 @@ Dictionary::Ptr HttpUtility::FetchRequestParameters(HttpRequest& request)
result = new Dictionary(); result = new Dictionary();
std::map<String, std::vector<String>> query; std::map<String, std::vector<String>> query;
for (const auto& kv : request.RequestUrl->GetQuery()) { for (const auto& kv : url->GetQuery()) {
query[kv.first].emplace_back(kv.second); 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()); response.WriteBody(body.CStr(), body.GetLength());
} }
void HttpUtility::SendJsonBody(boost::beast::http::response<boost::beast::http::string_body>& 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 HttpUtility::GetLastParameter(const Dictionary::Ptr& params, const String& key)
{ {
Value varr = params->Get(key); Value varr = params->Get(key);
@ -93,6 +98,24 @@ void HttpUtility::SendJsonError(HttpResponse& response, const Dictionary::Ptr& p
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
} }
void HttpUtility::SendJsonError(boost::beast::http::response<boost::beast::http::string_body>& 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) String HttpUtility::GetErrorNameByCode(const int code)
{ {
switch(code) { switch(code) {

View File

@ -5,7 +5,10 @@
#include "remote/httprequest.hpp" #include "remote/httprequest.hpp"
#include "remote/httpresponse.hpp" #include "remote/httpresponse.hpp"
#include "remote/url.hpp"
#include "base/dictionary.hpp" #include "base/dictionary.hpp"
#include <string>
#include <boost/beast/http.hpp>
namespace icinga namespace icinga
{ {
@ -19,11 +22,14 @@ class HttpUtility
{ {
public: 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(HttpResponse& response, const Dictionary::Ptr& params, const Value& val);
static void SendJsonBody(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const Value& val);
static Value GetLastParameter(const Dictionary::Ptr& params, const String& key); static Value GetLastParameter(const Dictionary::Ptr& params, const String& key);
static void SendJsonError(HttpResponse& response, const Dictionary::Ptr& params, const int code, static void SendJsonError(HttpResponse& response, const Dictionary::Ptr& params, const int code,
const String& verbose = String(), const String& diagnosticInformation = String()); const String& verbose = String(), const String& diagnosticInformation = String());
static void SendJsonError(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const int code,
const String& verbose = String(), const String& diagnosticInformation = String());
private: private:
static String GetErrorNameByCode(int code); static String GetErrorNameByCode(int code);

View File

@ -8,24 +8,35 @@ using namespace icinga;
REGISTER_URLHANDLER("/", InfoHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() > 2) namespace http = boost::beast::http;
if (url->GetPath().size() > 2)
return false; return false;
if (request.RequestMethod != "GET") if (request.method() != http::verb::get)
return false; return false;
if (request.RequestUrl->GetPath().empty()) { if (url->GetPath().empty()) {
response.SetStatus(302, "Found"); response.result(http::status::found);
response.AddHeader("Location", "/v1"); response.set(http::field::location, "/v1");
return true; return true;
} }
if (request.RequestUrl->GetPath()[0] != "v1" || request.RequestUrl->GetPath().size() != 1) if (url->GetPath()[0] != "v1" || url->GetPath().size() != 1)
return false; return false;
response.SetStatus(200, "OK"); response.result(http::status::ok);
std::vector<String> permInfo; std::vector<String> permInfo;
Array::Ptr permissions = user->GetPermissions(); 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({ Dictionary::Ptr result1 = new Dictionary({
{ "user", user->GetName() }, { "user", user->GetName() },
{ "permissions", Array::FromVector(permInfo) }, { "permissions", Array::FromVector(permInfo) },
@ -63,7 +74,7 @@ bool InfoHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request,
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
} else { } else {
response.AddHeader("Content-Type", "text/html"); response.set(http::field::content_type, "text/html");
String body = "<html><head><title>Icinga 2</title></head><h1>Hello from Icinga 2 (Version: " + Application::GetAppVersion() + ")!</h1>"; String body = "<html><head><title>Icinga 2</title></head><h1>Hello from Icinga 2 (Version: " + Application::GetAppVersion() + ")!</h1>";
body += "<p>You are authenticated as <b>" + user->GetName() + "</b>. "; body += "<p>You are authenticated as <b>" + user->GetName() + "</b>. ";
@ -80,7 +91,8 @@ bool InfoHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request,
body += "Your user does not have any permissions.</p>"; body += "Your user does not have any permissions.</p>";
body += R"(<p>More information about API requests is available in the <a href="https://docs.icinga.com/icinga2/latest" target="_blank">documentation</a>.</p></html>)"; body += R"(<p>More information about API requests is available in the <a href="https://docs.icinga.com/icinga2/latest" target="_blank">documentation</a>.</p></html>)";
response.WriteBody(body.CStr(), body.GetLength()); response.body() = body;
response.set(http::field::content_length, response.body().size());
} }
return true; return true;

View File

@ -13,8 +13,16 @@ class InfoHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(InfoHandler); DECLARE_PTR_TYPEDEFS(InfoHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -6,7 +6,11 @@
#include "base/console.hpp" #include "base/console.hpp"
#include "base/scriptglobal.hpp" #include "base/scriptglobal.hpp"
#include "base/convert.hpp" #include "base/convert.hpp"
#include "base/tlsstream.hpp"
#include <iostream> #include <iostream>
#include <memory>
#include <utility>
#include <boost/asio/spawn.hpp>
using namespace icinga; using namespace icinga;
@ -55,6 +59,35 @@ size_t JsonRpc::SendMessage(const Stream::Ptr& stream, const Dictionary::Ptr& me
return NetString::WriteStringToStream(stream, json); 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<AsioTlsStream>& 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<AsioTlsStream>& 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) StreamReadStatus JsonRpc::ReadMessage(const Stream::Ptr& stream, String *message, StreamReadContext& src, bool may_wait, ssize_t maxMessageLength)
{ {
String jsonString; String jsonString;
@ -73,6 +106,18 @@ StreamReadStatus JsonRpc::ReadMessage(const Stream::Ptr& stream, String *message
return StatusNewItem; return StatusNewItem;
} }
String JsonRpc::ReadMessage(const std::shared_ptr<AsioTlsStream>& 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) Dictionary::Ptr JsonRpc::DecodeMessage(const String& message)
{ {
Value value = JsonDecode(message); Value value = JsonDecode(message);

View File

@ -5,7 +5,10 @@
#include "base/stream.hpp" #include "base/stream.hpp"
#include "base/dictionary.hpp" #include "base/dictionary.hpp"
#include "base/tlsstream.hpp"
#include "remote/i2-remote.hpp" #include "remote/i2-remote.hpp"
#include <memory>
#include <boost/asio/spawn.hpp>
namespace icinga namespace icinga
{ {
@ -19,7 +22,10 @@ class JsonRpc
{ {
public: public:
static size_t SendMessage(const Stream::Ptr& stream, const Dictionary::Ptr& message); static size_t SendMessage(const Stream::Ptr& stream, const Dictionary::Ptr& message);
static size_t SendMessage(const std::shared_ptr<AsioTlsStream>& stream, const Dictionary::Ptr& message, boost::asio::yield_context yc);
static size_t SendRawMessage(const std::shared_ptr<AsioTlsStream>& 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 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<AsioTlsStream>& stream, boost::asio::yield_context yc, ssize_t maxMessageLength = -1);
static Dictionary::Ptr DecodeMessage(const String& message); static Dictionary::Ptr DecodeMessage(const String& message);
private: private:

View File

@ -7,34 +7,42 @@
#include "base/configtype.hpp" #include "base/configtype.hpp"
#include "base/logger.hpp" #include "base/logger.hpp"
#include "base/utility.hpp" #include "base/utility.hpp"
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
using namespace icinga; using namespace icinga;
REGISTER_APIFUNCTION(Heartbeat, event, &JsonRpcConnection::HeartbeatAPIHandler); REGISTER_APIFUNCTION(Heartbeat, event, &JsonRpcConnection::HeartbeatAPIHandler);
void JsonRpcConnection::HeartbeatTimerHandler() void JsonRpcConnection::HandleAndWriteHeartbeats(boost::asio::yield_context yc)
{ {
for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) { boost::asio::deadline_timer timer (m_Stream->get_io_service());
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.";
client->Disconnect(); for (;;) {
continue; timer.expires_from_now(boost::posix_time::seconds(10));
timer.async_wait(yc);
if (m_ShuttingDown) {
break;
} }
Dictionary::Ptr request = new Dictionary({ 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" }, { "jsonrpc", "2.0" },
{ "method", "event::Heartbeat" }, { "method", "event::Heartbeat" },
{ "params", new Dictionary({ { "params", new Dictionary({
{ "timeout", 120 } { "timeout", 120 }
}) } }) }
}); }));
client->SendMessage(request);
}
} }
} }
@ -44,7 +52,6 @@ Value JsonRpcConnection::HeartbeatAPIHandler(const MessageOrigin::Ptr& origin, c
if (!vtimeout.IsEmpty()) { if (!vtimeout.IsEmpty()) {
origin->FromClient->m_NextHeartbeat = Utility::GetTime() + vtimeout; origin->FromClient->m_NextHeartbeat = Utility::GetTime() + vtimeout;
origin->FromClient->m_HeartbeatTimeout = vtimeout;
} }
return Empty; return Empty;

View File

@ -13,6 +13,8 @@
#include <boost/thread/once.hpp> #include <boost/thread/once.hpp>
#include <boost/regex.hpp> #include <boost/regex.hpp>
#include <fstream> #include <fstream>
#include <openssl/ssl.h>
#include <openssl/x509.h>
using namespace icinga; using namespace icinga;
@ -30,10 +32,12 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
Dictionary::Ptr result = new Dictionary(); Dictionary::Ptr result = new Dictionary();
/* Use the presented client certificate if not provided. */ /* Use the presented client certificate if not provided. */
if (certText.IsEmpty()) if (certText.IsEmpty()) {
cert = origin->FromClient->GetStream()->GetPeerCertificate(); auto stream (origin->FromClient->GetStream());
else cert = std::shared_ptr<X509>(SSL_get_peer_certificate(stream->next_layer().native_handle()), X509_free);
} else {
cert = StringToCertificate(certText); cert = StringToCertificate(certText);
}
if (!cert) { if (!cert) {
Log(LogWarning, "JsonRpcConnection") << "No certificate or CSR received"; Log(LogWarning, "JsonRpcConnection") << "No certificate or CSR received";
@ -121,7 +125,7 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
{ "method", "pki::UpdateCertificate" }, { "method", "pki::UpdateCertificate" },
{ "params", result } { "params", result }
}); });
JsonRpc::SendMessage(client->GetStream(), message); client->SendMessage(message);
return result; return result;
} }
@ -192,7 +196,7 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
{ "method", "pki::UpdateCertificate" }, { "method", "pki::UpdateCertificate" },
{ "params", result } { "params", result }
}); });
JsonRpc::SendMessage(client->GetStream(), message); client->SendMessage(message);
return result; return result;
@ -255,7 +259,7 @@ void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& acl
* or b) the local zone and all parents. * or b) the local zone and all parents.
*/ */
if (aclient) if (aclient)
JsonRpc::SendMessage(aclient->GetStream(), message); aclient->SendMessage(message);
else else
listener->RelayMessage(origin, Zone::GetLocalZone(), message, false); listener->RelayMessage(origin, Zone::GetLocalZone(), message, false);
} }

View File

@ -4,12 +4,21 @@
#include "remote/apilistener.hpp" #include "remote/apilistener.hpp"
#include "remote/apifunction.hpp" #include "remote/apifunction.hpp"
#include "remote/jsonrpc.hpp" #include "remote/jsonrpc.hpp"
#include "base/defer.hpp"
#include "base/configtype.hpp" #include "base/configtype.hpp"
#include "base/io-engine.hpp"
#include "base/json.hpp"
#include "base/objectlock.hpp" #include "base/objectlock.hpp"
#include "base/utility.hpp" #include "base/utility.hpp"
#include "base/logger.hpp" #include "base/logger.hpp"
#include "base/exception.hpp" #include "base/exception.hpp"
#include "base/convert.hpp" #include "base/convert.hpp"
#include "base/tlsstream.hpp"
#include <memory>
#include <utility>
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/thread/once.hpp> #include <boost/thread/once.hpp>
using namespace icinga; using namespace icinga;
@ -17,50 +26,108 @@ using namespace icinga;
static Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params); static Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler); REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler);
static boost::once_flag l_JsonRpcConnectionOnceFlag = BOOST_ONCE_INIT; static RingBuffer l_TaskStats (15 * 60);
static Timer::Ptr l_JsonRpcConnectionTimeoutTimer;
static WorkQueue *l_JsonRpcConnectionWorkQueues;
static size_t l_JsonRpcConnectionWorkQueueCount;
static int l_JsonRpcConnectionNextID;
static Timer::Ptr l_HeartbeatTimer;
JsonRpcConnection::JsonRpcConnection(const String& identity, bool authenticated, JsonRpcConnection::JsonRpcConnection(const String& identity, bool authenticated,
TlsStream::Ptr stream, ConnectionRole role) const std::shared_ptr<AsioTlsStream>& stream, ConnectionRole role)
: m_ID(l_JsonRpcConnectionNextID++), m_Identity(identity), m_Authenticated(authenticated), m_Stream(std::move(stream)), : m_Identity(identity), m_Authenticated(authenticated), m_Stream(stream),
m_Role(role), m_Timestamp(Utility::GetTime()), m_Seen(Utility::GetTime()), m_NextHeartbeat(0), m_HeartbeatTimeout(0) 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) if (authenticated)
m_Endpoint = Endpoint::GetByName(identity); 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() void JsonRpcConnection::Start()
{ {
/* the stream holds an owning reference to this object through the callback we're registering here */ namespace asio = boost::asio;
m_Stream->RegisterDataHandler(std::bind(&JsonRpcConnection::DataAvailableHandler, JsonRpcConnection::Ptr(this)));
if (m_Stream->IsDataAvailable()) JsonRpcConnection::Ptr keepAlive (this);
DataAvailableHandler();
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 double JsonRpcConnection::GetTimestamp() const
@ -83,7 +150,7 @@ Endpoint::Ptr JsonRpcConnection::GetEndpoint() const
return m_Endpoint; return m_Endpoint;
} }
TlsStream::Ptr JsonRpcConnection::GetStream() const std::shared_ptr<AsioTlsStream> JsonRpcConnection::GetStream() const
{ {
return m_Stream; return m_Stream;
} }
@ -95,69 +162,66 @@ ConnectionRole JsonRpcConnection::GetRole() const
void JsonRpcConnection::SendMessage(const Dictionary::Ptr& message) void JsonRpcConnection::SendMessage(const Dictionary::Ptr& message)
{ {
try { m_IoStrand.post([this, message]() { SendMessageInternal(message); });
ObjectLock olock(m_Stream);
if (m_Stream->IsEof())
return;
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::SendRawMessage(const String& message)
{
m_IoStrand.post([this, message]() {
m_OutgoingMessagesQueue.emplace_back(message);
m_OutgoingMessagesQueued.Set();
});
}
void JsonRpcConnection::SendMessageInternal(const Dictionary::Ptr& message)
{
m_OutgoingMessagesQueue.emplace_back(JsonEncode(message));
m_OutgoingMessagesQueued.Set();
} }
void JsonRpcConnection::Disconnect() void JsonRpcConnection::Disconnect()
{ {
namespace asio = boost::asio;
JsonRpcConnection::Ptr keepAlive (this);
asio::spawn(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
if (!m_ShuttingDown) {
m_ShuttingDown = true;
Log(LogWarning, "JsonRpcConnection") Log(LogWarning, "JsonRpcConnection")
<< "API client disconnected for identity '" << m_Identity << "'"; << "API client disconnected for identity '" << m_Identity << "'";
m_Stream->Close(); m_OutgoingMessagesQueued.Set();
if (m_Endpoint) m_WriterDone.Wait(yc);
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); m_Endpoint->RemoveClient(this);
else { } else {
ApiListener::Ptr listener = ApiListener::GetInstance(); auto listener (ApiListener::GetInstance());
listener->RemoveAnonymousClient(this); listener->RemoveAnonymousClient(this);
} }
} }
});
void JsonRpcConnection::MessageHandlerWrapper(const String& jsonString)
{
if (m_Stream->IsEof())
return;
try {
MessageHandler(jsonString);
} catch (const std::exception& ex) {
Log(LogWarning, "JsonRpcConnection")
<< "Error while reading JSON-RPC message for identity '" << m_Identity
<< "': " << DiagnosticInformation(ex);
Disconnect();
return;
}
} }
void JsonRpcConnection::MessageHandler(const String& jsonString) void JsonRpcConnection::MessageHandler(const String& jsonString)
{ {
Dictionary::Ptr message = JsonRpc::DecodeMessage(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")) { if (m_Endpoint && message->Contains("ts")) {
double ts = message->Get("ts"); double ts = message->Get("ts");
@ -225,59 +289,11 @@ void JsonRpcConnection::MessageHandler(const String& jsonString)
if (message->Contains("id")) { if (message->Contains("id")) {
resultMessage->Set("jsonrpc", "2.0"); resultMessage->Set("jsonrpc", "2.0");
resultMessage->Set("id", message->Get("id")); 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) Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{ {
double log_position = params->Get("log_position"); double log_position = params->Get("log_position");
@ -292,57 +308,29 @@ Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::
return Empty; return Empty;
} }
void JsonRpcConnection::CheckLiveness() void JsonRpcConnection::CheckLiveness(boost::asio::yield_context yc)
{ {
boost::asio::deadline_timer timer (m_Stream->get_io_service());
for (;;) {
timer.expires_from_now(boost::posix_time::seconds(30));
timer.async_wait(yc);
if (m_ShuttingDown) {
break;
}
if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) { if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) {
Log(LogInformation, "JsonRpcConnection") Log(LogInformation, "JsonRpcConnection")
<< "No messages for identity '" << m_Identity << "' have been received in the last 60 seconds."; << "No messages for identity '" << m_Identity << "' have been received in the last 60 seconds.";
Disconnect(); Disconnect();
break;
} }
} }
void JsonRpcConnection::TimeoutTimerHandler()
{
ApiListener::Ptr listener = ApiListener::GetInstance();
for (const JsonRpcConnection::Ptr& client : listener->GetAnonymousClients()) {
client->CheckLiveness();
}
for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
client->CheckLiveness();
}
}
}
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 JsonRpcConnection::GetWorkQueueRate()
{ {
double rate = 0.0; return l_TaskStats.UpdateAndGetValues(Utility::GetTime(), 60) / 60.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;
} }

View File

@ -5,9 +5,14 @@
#include "remote/i2-remote.hpp" #include "remote/i2-remote.hpp"
#include "remote/endpoint.hpp" #include "remote/endpoint.hpp"
#include "base/io-engine.hpp"
#include "base/tlsstream.hpp" #include "base/tlsstream.hpp"
#include "base/timer.hpp" #include "base/timer.hpp"
#include "base/workqueue.hpp" #include "base/workqueue.hpp"
#include <memory>
#include <vector>
#include <boost/asio/io_service_strand.hpp>
#include <boost/asio/spawn.hpp>
namespace icinga namespace icinga
{ {
@ -36,7 +41,7 @@ class JsonRpcConnection final : public Object
public: public:
DECLARE_PTR_TYPEDEFS(JsonRpcConnection); 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<AsioTlsStream>& stream, ConnectionRole role);
void Start(); void Start();
@ -44,47 +49,46 @@ public:
String GetIdentity() const; String GetIdentity() const;
bool IsAuthenticated() const; bool IsAuthenticated() const;
Endpoint::Ptr GetEndpoint() const; Endpoint::Ptr GetEndpoint() const;
TlsStream::Ptr GetStream() const; std::shared_ptr<AsioTlsStream> GetStream() const;
ConnectionRole GetRole() const; ConnectionRole GetRole() const;
void Disconnect(); void Disconnect();
void SendMessage(const Dictionary::Ptr& request); void SendMessage(const Dictionary::Ptr& request);
void SendRawMessage(const String& request);
static void HeartbeatTimerHandler();
static Value HeartbeatAPIHandler(const intrusive_ptr<MessageOrigin>& origin, const Dictionary::Ptr& params); static Value HeartbeatAPIHandler(const intrusive_ptr<MessageOrigin>& origin, const Dictionary::Ptr& params);
static size_t GetWorkQueueCount();
static size_t GetWorkQueueLength();
static double GetWorkQueueRate(); static double GetWorkQueueRate();
static void SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const intrusive_ptr<MessageOrigin>& origin, const String& path); static void SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const intrusive_ptr<MessageOrigin>& origin, const String& path);
private: private:
int m_ID;
String m_Identity; String m_Identity;
bool m_Authenticated; bool m_Authenticated;
Endpoint::Ptr m_Endpoint; Endpoint::Ptr m_Endpoint;
TlsStream::Ptr m_Stream; std::shared_ptr<AsioTlsStream> m_Stream;
ConnectionRole m_Role; ConnectionRole m_Role;
double m_Timestamp; double m_Timestamp;
double m_Seen; double m_Seen;
double m_NextHeartbeat; double m_NextHeartbeat;
double m_HeartbeatTimeout; boost::asio::io_service::strand m_IoStrand;
boost::mutex m_DataHandlerMutex; std::vector<String> 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(); bool ProcessMessage();
void MessageHandlerWrapper(const String& jsonString);
void MessageHandler(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 CertificateRequestResponseHandler(const Dictionary::Ptr& message);
void SendMessageInternal(const Dictionary::Ptr& request);
}; };
} }

View File

@ -12,15 +12,26 @@ using namespace icinga;
REGISTER_URLHANDLER("/v1/objects", ModifyObjectHandler); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
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; return false;
if (request.RequestMethod != "POST") if (request.method() != http::verb::post)
return false; return false;
Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
if (!type) { if (!type) {
HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); 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()); params->Set("type", type->GetName());
if (request.RequestUrl->GetPath().size() >= 4) { if (url->GetPath().size() >= 4) {
String attr = type->GetName(); String attr = type->GetName();
boost::algorithm::to_lower(attr); boost::algorithm::to_lower(attr);
params->Set(attr, request.RequestUrl->GetPath()[3]); params->Set(attr, url->GetPath()[3]);
} }
std::vector<Value> objs; std::vector<Value> objs;
@ -101,7 +112,7 @@ bool ModifyObjectHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& r
{ "results", new Array(std::move(results)) } { "results", new Array(std::move(results)) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
return true; return true;

View File

@ -13,8 +13,16 @@ class ModifyObjectHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(ModifyObjectHandler); DECLARE_PTR_TYPEDEFS(ModifyObjectHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -87,15 +87,26 @@ Dictionary::Ptr ObjectQueryHandler::SerializeObjectAttrs(const Object::Ptr& obje
return new Dictionary(std::move(resultAttrs)); 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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
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; return false;
if (request.RequestMethod != "GET") if (request.method() != http::verb::get)
return false; return false;
Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
if (!type) { if (!type) {
HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); 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()); params->Set("type", type->GetName());
if (request.RequestUrl->GetPath().size() >= 4) { if (url->GetPath().size() >= 4) {
String attr = type->GetName(); String attr = type->GetName();
boost::algorithm::to_lower(attr); boost::algorithm::to_lower(attr);
params->Set(attr, request.RequestUrl->GetPath()[3]); params->Set(attr, url->GetPath()[3]);
} }
std::vector<Value> objs; std::vector<Value> objs;
@ -265,7 +276,7 @@ bool ObjectQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& re
{ "results", new Array(std::move(results)) } { "results", new Array(std::move(results)) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
return true; return true;

View File

@ -13,8 +13,16 @@ class ObjectQueryHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(ObjectQueryHandler); DECLARE_PTR_TYPEDEFS(ObjectQueryHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
private: private:
static Dictionary::Ptr SerializeObjectAttrs(const Object::Ptr& object, const String& attrPrefix, static Dictionary::Ptr SerializeObjectAttrs(const Object::Ptr& object, const String& attrPrefix,

View File

@ -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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() > 3) namespace http = boost::beast::http;
if (url->GetPath().size() > 3)
return false; return false;
if (request.RequestMethod != "GET") if (request.method() != http::verb::get)
return false; return false;
QueryDescription qd; QueryDescription qd;
@ -83,8 +94,8 @@ bool StatusHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request
params->Set("type", "Status"); params->Set("type", "Status");
if (request.RequestUrl->GetPath().size() >= 3) if (url->GetPath().size() >= 3)
params->Set("status", request.RequestUrl->GetPath()[2]); params->Set("status", url->GetPath()[2]);
std::vector<Value> objs; std::vector<Value> objs;
@ -101,7 +112,7 @@ bool StatusHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request
{ "results", new Array(std::move(objs)) } { "results", new Array(std::move(objs)) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
return true; return true;

View File

@ -13,8 +13,16 @@ class StatusHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(StatusHandler); DECLARE_PTR_TYPEDEFS(StatusHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
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; return false;
if (request.RequestMethod != "GET") if (request.method() != http::verb::get)
return false; return false;
Type::Ptr type = FilterUtility::TypeFromPluralName(request.RequestUrl->GetPath()[2]); Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
if (!type) { if (!type) {
HttpUtility::SendJsonError(response, params, 400, "Invalid type specified."); 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()); params->Set("type", type->GetName());
if (request.RequestUrl->GetPath().size() >= 4) { if (url->GetPath().size() >= 4) {
String attr = type->GetName(); String attr = type->GetName();
boost::algorithm::to_lower(attr); boost::algorithm::to_lower(attr);
params->Set(attr, request.RequestUrl->GetPath()[3]); params->Set(attr, url->GetPath()[3]);
} }
std::vector<Value> objs; std::vector<Value> objs;
@ -118,7 +129,7 @@ bool TemplateQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest&
{ "results", new Array(std::move(objs)) } { "results", new Array(std::move(objs)) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
return true; return true;

View File

@ -13,8 +13,16 @@ class TemplateQueryHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(TemplateQueryHandler); DECLARE_PTR_TYPEDEFS(TemplateQueryHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() > 3) namespace http = boost::beast::http;
if (url->GetPath().size() > 3)
return false; return false;
if (request.RequestMethod != "GET") if (request.method() != http::verb::get)
return false; return false;
QueryDescription qd; QueryDescription qd;
@ -64,8 +75,8 @@ bool TypeQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& requ
params->Set("type", "Type"); params->Set("type", "Type");
if (request.RequestUrl->GetPath().size() >= 3) if (url->GetPath().size() >= 3)
params->Set("name", request.RequestUrl->GetPath()[2]); params->Set("name", url->GetPath()[2]);
std::vector<Value> objs; std::vector<Value> objs;
@ -138,7 +149,7 @@ bool TypeQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& requ
{ "results", new Array(std::move(results)) } { "results", new Array(std::move(results)) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
return true; return true;

View File

@ -13,8 +13,16 @@ class TypeQueryHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(TypeQueryHandler); DECLARE_PTR_TYPEDEFS(TypeQueryHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -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<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
)
{ {
if (request.RequestUrl->GetPath().size() > 3) namespace http = boost::beast::http;
if (url->GetPath().size() > 3)
return false; return false;
if (request.RequestMethod != "GET") if (request.method() != http::verb::get)
return false; return false;
QueryDescription qd; QueryDescription qd;
@ -71,8 +82,8 @@ bool VariableQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest&
params->Set("type", "Variable"); params->Set("type", "Variable");
if (request.RequestUrl->GetPath().size() >= 3) if (url->GetPath().size() >= 3)
params->Set("variable", request.RequestUrl->GetPath()[2]); params->Set("variable", url->GetPath()[2]);
std::vector<Value> objs; std::vector<Value> objs;
@ -99,7 +110,7 @@ bool VariableQueryHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest&
{ "results", new Array(std::move(results)) } { "results", new Array(std::move(results)) }
}); });
response.SetStatus(200, "OK"); response.result(http::status::ok);
HttpUtility::SendJsonBody(response, params, result); HttpUtility::SendJsonBody(response, params, result);
return true; return true;

View File

@ -13,8 +13,16 @@ class VariableQueryHandler final : public HttpHandler
public: public:
DECLARE_PTR_TYPEDEFS(VariableQueryHandler); DECLARE_PTR_TYPEDEFS(VariableQueryHandler);
bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, bool HandleRequest(
HttpResponse& response, const Dictionary::Ptr& params) override; AsioTlsStream& stream,
const ApiUser::Ptr& user,
boost::beast::http::request<boost::beast::http::string_body>& request,
const Url::Ptr& url,
boost::beast::http::response<boost::beast::http::string_body>& response,
const Dictionary::Ptr& params,
boost::asio::yield_context& yc,
bool& hasStartedStreaming
) override;
}; };
} }

View File

@ -9,7 +9,6 @@ add_executable(check_nscp_api
target_link_libraries(check_nscp_api ${base_DEPS}) target_link_libraries(check_nscp_api ${base_DEPS})
set_target_properties ( set_target_properties (
check_nscp_api PROPERTIES check_nscp_api PROPERTIES
INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2
DEFINE_SYMBOL I2_PLUGINS_BUILD DEFINE_SYMBOL I2_PLUGINS_BUILD
FOLDER Plugins) FOLDER Plugins)
@ -32,7 +31,6 @@ if (WIN32)
set_target_properties( set_target_properties(
thresholds PROPERTIES thresholds PROPERTIES
INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2
FOLDER Plugins FOLDER Plugins
) )
@ -50,7 +48,6 @@ if (WIN32)
set_target_properties( set_target_properties(
${check_OUT} PROPERTIES ${check_OUT} PROPERTIES
INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2
DEFINE_SYMBOL I2_PLUGINS_BUILD DEFINE_SYMBOL I2_PLUGINS_BUILD
FOLDER Plugins FOLDER Plugins
) )