summaryrefslogtreecommitdiffstats
path: root/lib/remote
diff options
context:
space:
mode:
Diffstat (limited to 'lib/remote')
-rw-r--r--lib/remote/CMakeLists.txt67
-rw-r--r--lib/remote/actionshandler.cpp145
-rw-r--r--lib/remote/actionshandler.hpp32
-rw-r--r--lib/remote/apiaction.cpp40
-rw-r--r--lib/remote/apiaction.hpp69
-rw-r--r--lib/remote/apifunction.cpp35
-rw-r--r--lib/remote/apifunction.hpp59
-rw-r--r--lib/remote/apilistener-authority.cpp84
-rw-r--r--lib/remote/apilistener-configsync.cpp464
-rw-r--r--lib/remote/apilistener-filesync.cpp887
-rw-r--r--lib/remote/apilistener.cpp1970
-rw-r--r--lib/remote/apilistener.hpp265
-rw-r--r--lib/remote/apilistener.ti66
-rw-r--r--lib/remote/apiuser.cpp55
-rw-r--r--lib/remote/apiuser.hpp27
-rw-r--r--lib/remote/apiuser.ti31
-rw-r--r--lib/remote/configfileshandler.cpp94
-rw-r--r--lib/remote/configfileshandler.hpp30
-rw-r--r--lib/remote/configobjectslock.cpp24
-rw-r--r--lib/remote/configobjectslock.hpp72
-rw-r--r--lib/remote/configobjectutility.cpp377
-rw-r--r--lib/remote/configobjectutility.hpp47
-rw-r--r--lib/remote/configpackageshandler.cpp179
-rw-r--r--lib/remote/configpackageshandler.hpp54
-rw-r--r--lib/remote/configpackageutility.cpp413
-rw-r--r--lib/remote/configpackageutility.hpp73
-rw-r--r--lib/remote/configstageshandler.cpp225
-rw-r--r--lib/remote/configstageshandler.hpp56
-rw-r--r--lib/remote/consolehandler.cpp327
-rw-r--r--lib/remote/consolehandler.hpp50
-rw-r--r--lib/remote/createobjecthandler.cpp155
-rw-r--r--lib/remote/createobjecthandler.hpp30
-rw-r--r--lib/remote/deleteobjecthandler.cpp123
-rw-r--r--lib/remote/deleteobjecthandler.hpp30
-rw-r--r--lib/remote/endpoint.cpp138
-rw-r--r--lib/remote/endpoint.hpp68
-rw-r--r--lib/remote/endpoint.ti59
-rw-r--r--lib/remote/eventqueue.cpp351
-rw-r--r--lib/remote/eventqueue.hpp177
-rw-r--r--lib/remote/eventshandler.cpp137
-rw-r--r--lib/remote/eventshandler.hpp31
-rw-r--r--lib/remote/filterutility.cpp354
-rw-r--r--lib/remote/filterutility.hpp64
-rw-r--r--lib/remote/httphandler.cpp129
-rw-r--r--lib/remote/httphandler.hpp74
-rw-r--r--lib/remote/httpserverconnection.cpp613
-rw-r--r--lib/remote/httpserverconnection.hpp54
-rw-r--r--lib/remote/httputility.cpp80
-rw-r--r--lib/remote/httputility.hpp33
-rw-r--r--lib/remote/i2-remote.hpp14
-rw-r--r--lib/remote/infohandler.cpp100
-rw-r--r--lib/remote/infohandler.hpp30
-rw-r--r--lib/remote/jsonrpc.cpp157
-rw-r--r--lib/remote/jsonrpc.hpp39
-rw-r--r--lib/remote/jsonrpcconnection-heartbeat.cpp48
-rw-r--r--lib/remote/jsonrpcconnection-pki.cpp439
-rw-r--r--lib/remote/jsonrpcconnection.cpp388
-rw-r--r--lib/remote/jsonrpcconnection.hpp100
-rw-r--r--lib/remote/messageorigin.cpp10
-rw-r--r--lib/remote/messageorigin.hpp28
-rw-r--r--lib/remote/modifyobjecthandler.cpp168
-rw-r--r--lib/remote/modifyobjecthandler.hpp30
-rw-r--r--lib/remote/objectqueryhandler.cpp330
-rw-r--r--lib/remote/objectqueryhandler.hpp34
-rw-r--r--lib/remote/pkiutility.cpp452
-rw-r--r--lib/remote/pkiutility.hpp41
-rw-r--r--lib/remote/statushandler.cpp120
-rw-r--r--lib/remote/statushandler.hpp30
-rw-r--r--lib/remote/templatequeryhandler.cpp136
-rw-r--r--lib/remote/templatequeryhandler.hpp30
-rw-r--r--lib/remote/typequeryhandler.cpp156
-rw-r--r--lib/remote/typequeryhandler.hpp30
-rw-r--r--lib/remote/url-characters.hpp29
-rw-r--r--lib/remote/url.cpp363
-rw-r--r--lib/remote/url.hpp78
-rw-r--r--lib/remote/variablequeryhandler.cpp121
-rw-r--r--lib/remote/variablequeryhandler.hpp30
-rw-r--r--lib/remote/zone.cpp154
-rw-r--r--lib/remote/zone.hpp46
-rw-r--r--lib/remote/zone.ti25
80 files changed, 12773 insertions, 0 deletions
diff --git a/lib/remote/CMakeLists.txt b/lib/remote/CMakeLists.txt
new file mode 100644
index 0000000..740b112
--- /dev/null
+++ b/lib/remote/CMakeLists.txt
@@ -0,0 +1,67 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+mkclass_target(apilistener.ti apilistener-ti.cpp apilistener-ti.hpp)
+mkclass_target(apiuser.ti apiuser-ti.cpp apiuser-ti.hpp)
+mkclass_target(endpoint.ti endpoint-ti.cpp endpoint-ti.hpp)
+mkclass_target(zone.ti zone-ti.cpp zone-ti.hpp)
+
+set(remote_SOURCES
+ i2-remote.hpp
+ actionshandler.cpp actionshandler.hpp
+ apiaction.cpp apiaction.hpp
+ apifunction.cpp apifunction.hpp
+ apilistener.cpp apilistener.hpp apilistener-ti.hpp apilistener-configsync.cpp apilistener-filesync.cpp
+ apilistener-authority.cpp
+ apiuser.cpp apiuser.hpp apiuser-ti.hpp
+ configfileshandler.cpp configfileshandler.hpp
+ configobjectslock.cpp configobjectslock.hpp
+ configobjectutility.cpp configobjectutility.hpp
+ configpackageshandler.cpp configpackageshandler.hpp
+ configpackageutility.cpp configpackageutility.hpp
+ configstageshandler.cpp configstageshandler.hpp
+ consolehandler.cpp consolehandler.hpp
+ createobjecthandler.cpp createobjecthandler.hpp
+ deleteobjecthandler.cpp deleteobjecthandler.hpp
+ endpoint.cpp endpoint.hpp endpoint-ti.hpp
+ eventqueue.cpp eventqueue.hpp
+ eventshandler.cpp eventshandler.hpp
+ filterutility.cpp filterutility.hpp
+ httphandler.cpp httphandler.hpp
+ httpserverconnection.cpp httpserverconnection.hpp
+ httputility.cpp httputility.hpp
+ infohandler.cpp infohandler.hpp
+ jsonrpc.cpp jsonrpc.hpp
+ jsonrpcconnection.cpp jsonrpcconnection.hpp jsonrpcconnection-heartbeat.cpp jsonrpcconnection-pki.cpp
+ messageorigin.cpp messageorigin.hpp
+ modifyobjecthandler.cpp modifyobjecthandler.hpp
+ objectqueryhandler.cpp objectqueryhandler.hpp
+ pkiutility.cpp pkiutility.hpp
+ statushandler.cpp statushandler.hpp
+ templatequeryhandler.cpp templatequeryhandler.hpp
+ typequeryhandler.cpp typequeryhandler.hpp
+ url.cpp url.hpp url-characters.hpp
+ variablequeryhandler.cpp variablequeryhandler.hpp
+ zone.cpp zone.hpp zone-ti.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(remote remote remote_SOURCES)
+endif()
+
+add_library(remote OBJECT ${remote_SOURCES})
+
+add_dependencies(remote base config)
+
+set_target_properties (
+ remote PROPERTIES
+ FOLDER Lib
+)
+
+#install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api/log\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api/zones\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api/zones-stage\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/certs\")")
+install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/certificate-requests\")")
+
+
diff --git a/lib/remote/actionshandler.cpp b/lib/remote/actionshandler.cpp
new file mode 100644
index 0000000..016c76d
--- /dev/null
+++ b/lib/remote/actionshandler.cpp
@@ -0,0 +1,145 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/actionshandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/apiaction.hpp"
+#include "base/defer.hpp"
+#include "base/exception.hpp"
+#include "base/logger.hpp"
+#include <set>
+
+using namespace icinga;
+
+thread_local ApiUser::Ptr ActionsHandler::AuthenticatedApiUser;
+
+REGISTER_URLHANDLER("/v1/actions", ActionsHandler);
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() != 3)
+ return false;
+
+ if (request.method() != http::verb::post)
+ return false;
+
+ String actionName = url->GetPath()[2];
+
+ ApiAction::Ptr action = ApiAction::GetByName(actionName);
+
+ if (!action) {
+ HttpUtility::SendJsonError(response, params, 404, "Action '" + actionName + "' does not exist.");
+ return true;
+ }
+
+ QueryDescription qd;
+
+ const std::vector<String>& types = action->GetTypes();
+ std::vector<Value> objs;
+
+ String permission = "actions/" + actionName;
+
+ if (!types.empty()) {
+ qd.Types = std::set<String>(types.begin(), types.end());
+ qd.Permission = permission;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ if (objs.empty()) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.");
+ return true;
+ }
+ } else {
+ FilterUtility::CheckPermission(user, permission);
+ objs.emplace_back(nullptr);
+ }
+
+ ArrayData results;
+
+ Log(LogNotice, "ApiActionHandler")
+ << "Running action " << actionName;
+
+ bool verbose = false;
+
+ ActionsHandler::AuthenticatedApiUser = user;
+ Defer a ([]() {
+ ActionsHandler::AuthenticatedApiUser = nullptr;
+ });
+
+ if (params)
+ verbose = HttpUtility::GetLastParameter(params, "verbose");
+
+ for (const ConfigObject::Ptr& obj : objs) {
+ try {
+ results.emplace_back(action->Invoke(obj, params));
+ } catch (const std::exception& ex) {
+ Dictionary::Ptr fail = new Dictionary({
+ { "code", 500 },
+ { "status", "Action execution failed: '" + DiagnosticInformation(ex, false) + "'." }
+ });
+
+ /* Exception for actions. Normally we would handle this inside SendJsonError(). */
+ if (verbose)
+ fail->Set("diagnostic_information", DiagnosticInformation(ex));
+
+ results.emplace_back(std::move(fail));
+ }
+ }
+
+ int statusCode = 500;
+ std::set<int> okStatusCodes, nonOkStatusCodes;
+
+ for (const Dictionary::Ptr& res : results) {
+ if (!res->Contains("code")) {
+ continue;
+ }
+
+ auto code = res->Get("code");
+
+ if (code >= 200 && code <= 299) {
+ okStatusCodes.insert(code);
+ } else {
+ nonOkStatusCodes.insert(code);
+ }
+ }
+
+ size_t okSize = okStatusCodes.size();
+ size_t nonOkSize = nonOkStatusCodes.size();
+
+ if (okSize == 1u && nonOkSize == 0u) {
+ statusCode = *okStatusCodes.begin();
+ } else if (nonOkSize == 1u) {
+ statusCode = *nonOkStatusCodes.begin();
+ } else if (okSize >= 2u && nonOkSize == 0u) {
+ statusCode = 200;
+ }
+
+ response.result(statusCode);
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/actionshandler.hpp b/lib/remote/actionshandler.hpp
new file mode 100644
index 0000000..ca662ca
--- /dev/null
+++ b/lib/remote/actionshandler.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ACTIONSHANDLER_H
+#define ACTIONSHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ActionsHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ActionsHandler);
+
+ static thread_local ApiUser::Ptr AuthenticatedApiUser;
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* ACTIONSHANDLER_H */
diff --git a/lib/remote/apiaction.cpp b/lib/remote/apiaction.cpp
new file mode 100644
index 0000000..4da91f0
--- /dev/null
+++ b/lib/remote/apiaction.cpp
@@ -0,0 +1,40 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apiaction.hpp"
+#include "base/singleton.hpp"
+
+using namespace icinga;
+
+ApiAction::ApiAction(std::vector<String> types, Callback action)
+ : m_Types(std::move(types)), m_Callback(std::move(action))
+{ }
+
+Value ApiAction::Invoke(const ConfigObject::Ptr& target, const Dictionary::Ptr& params)
+{
+ return m_Callback(target, params);
+}
+
+const std::vector<String>& ApiAction::GetTypes() const
+{
+ return m_Types;
+}
+
+ApiAction::Ptr ApiAction::GetByName(const String& name)
+{
+ return ApiActionRegistry::GetInstance()->GetItem(name);
+}
+
+void ApiAction::Register(const String& name, const ApiAction::Ptr& action)
+{
+ ApiActionRegistry::GetInstance()->Register(name, action);
+}
+
+void ApiAction::Unregister(const String& name)
+{
+ ApiActionRegistry::GetInstance()->Unregister(name);
+}
+
+ApiActionRegistry *ApiActionRegistry::GetInstance()
+{
+ return Singleton<ApiActionRegistry>::GetInstance();
+}
diff --git a/lib/remote/apiaction.hpp b/lib/remote/apiaction.hpp
new file mode 100644
index 0000000..f2719c1
--- /dev/null
+++ b/lib/remote/apiaction.hpp
@@ -0,0 +1,69 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIACTION_H
+#define APIACTION_H
+
+#include "remote/i2-remote.hpp"
+#include "base/registry.hpp"
+#include "base/value.hpp"
+#include "base/dictionary.hpp"
+#include "base/configobject.hpp"
+#include <vector>
+#include <boost/algorithm/string/replace.hpp>
+
+namespace icinga
+{
+
+/**
+ * An action available over the external HTTP API.
+ *
+ * @ingroup remote
+ */
+class ApiAction final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ApiAction);
+
+ typedef std::function<Value(const ConfigObject::Ptr& target, const Dictionary::Ptr& params)> Callback;
+
+ ApiAction(std::vector<String> registerTypes, Callback function);
+
+ Value Invoke(const ConfigObject::Ptr& target, const Dictionary::Ptr& params);
+
+ const std::vector<String>& GetTypes() const;
+
+ static ApiAction::Ptr GetByName(const String& name);
+ static void Register(const String& name, const ApiAction::Ptr& action);
+ static void Unregister(const String& name);
+
+private:
+ std::vector<String> m_Types;
+ Callback m_Callback;
+};
+
+/**
+ * A registry for API actions.
+ *
+ * @ingroup remote
+ */
+class ApiActionRegistry : public Registry<ApiActionRegistry, ApiAction::Ptr>
+{
+public:
+ static ApiActionRegistry *GetInstance();
+};
+
+#define REGISTER_APIACTION(name, types, callback) \
+ INITIALIZE_ONCE([]() { \
+ String registerName = #name; \
+ boost::algorithm::replace_all(registerName, "_", "-"); \
+ std::vector<String> registerTypes; \
+ String typeNames = types; \
+ if (!typeNames.IsEmpty()) \
+ registerTypes = typeNames.Split(";"); \
+ ApiAction::Ptr action = new ApiAction(registerTypes, callback); \
+ ApiActionRegistry::GetInstance()->Register(registerName, action); \
+ })
+
+}
+
+#endif /* APIACTION_H */
diff --git a/lib/remote/apifunction.cpp b/lib/remote/apifunction.cpp
new file mode 100644
index 0000000..5b855cc
--- /dev/null
+++ b/lib/remote/apifunction.cpp
@@ -0,0 +1,35 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apifunction.hpp"
+#include "base/singleton.hpp"
+
+using namespace icinga;
+
+ApiFunction::ApiFunction(Callback function)
+ : m_Callback(std::move(function))
+{ }
+
+Value ApiFunction::Invoke(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& arguments)
+{
+ return m_Callback(origin, arguments);
+}
+
+ApiFunction::Ptr ApiFunction::GetByName(const String& name)
+{
+ return ApiFunctionRegistry::GetInstance()->GetItem(name);
+}
+
+void ApiFunction::Register(const String& name, const ApiFunction::Ptr& function)
+{
+ ApiFunctionRegistry::GetInstance()->Register(name, function);
+}
+
+void ApiFunction::Unregister(const String& name)
+{
+ ApiFunctionRegistry::GetInstance()->Unregister(name);
+}
+
+ApiFunctionRegistry *ApiFunctionRegistry::GetInstance()
+{
+ return Singleton<ApiFunctionRegistry>::GetInstance();
+}
diff --git a/lib/remote/apifunction.hpp b/lib/remote/apifunction.hpp
new file mode 100644
index 0000000..e611320
--- /dev/null
+++ b/lib/remote/apifunction.hpp
@@ -0,0 +1,59 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIFUNCTION_H
+#define APIFUNCTION_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/messageorigin.hpp"
+#include "base/registry.hpp"
+#include "base/value.hpp"
+#include "base/dictionary.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * A function available over the internal cluster API.
+ *
+ * @ingroup base
+ */
+class ApiFunction final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ApiFunction);
+
+ typedef std::function<Value(const MessageOrigin::Ptr& origin, const Dictionary::Ptr&)> Callback;
+
+ ApiFunction(Callback function);
+
+ Value Invoke(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& arguments);
+
+ static ApiFunction::Ptr GetByName(const String& name);
+ static void Register(const String& name, const ApiFunction::Ptr& function);
+ static void Unregister(const String& name);
+
+private:
+ Callback m_Callback;
+};
+
+/**
+ * A registry for API functions.
+ *
+ * @ingroup base
+ */
+class ApiFunctionRegistry : public Registry<ApiFunctionRegistry, ApiFunction::Ptr>
+{
+public:
+ static ApiFunctionRegistry *GetInstance();
+};
+
+#define REGISTER_APIFUNCTION(name, ns, callback) \
+ INITIALIZE_ONCE([]() { \
+ ApiFunction::Ptr func = new ApiFunction(callback); \
+ ApiFunctionRegistry::GetInstance()->Register(#ns "::" #name, func); \
+ })
+
+}
+
+#endif /* APIFUNCTION_H */
diff --git a/lib/remote/apilistener-authority.cpp b/lib/remote/apilistener-authority.cpp
new file mode 100644
index 0000000..f33a190
--- /dev/null
+++ b/lib/remote/apilistener-authority.cpp
@@ -0,0 +1,84 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/zone.hpp"
+#include "remote/apilistener.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+std::atomic<bool> ApiListener::m_UpdatedObjectAuthority (false);
+
+void ApiListener::UpdateObjectAuthority()
+{
+ /* Always run this, even if there is no 'api' feature enabled. */
+ if (auto listener = ApiListener::GetInstance()) {
+ Log(LogNotice, "ApiListener")
+ << "Updating object authority for objects at endpoint '" << listener->GetIdentity() << "'.";
+ } else {
+ Log(LogNotice, "ApiListener")
+ << "Updating object authority for local objects.";
+ }
+
+ Zone::Ptr my_zone = Zone::GetLocalZone();
+
+ std::vector<Endpoint::Ptr> endpoints;
+ Endpoint::Ptr my_endpoint;
+
+ if (my_zone) {
+ my_endpoint = Endpoint::GetLocalEndpoint();
+
+ int num_total = 0;
+
+ for (const Endpoint::Ptr& endpoint : my_zone->GetEndpoints()) {
+ num_total++;
+
+ if (endpoint != my_endpoint && !endpoint->GetConnected())
+ continue;
+
+ endpoints.push_back(endpoint);
+ }
+
+ double startTime = Application::GetStartTime();
+
+ /* 30 seconds cold startup, don't update any authority to give the secondary endpoint time to reconnect. */
+ if (num_total > 1 && endpoints.size() <= 1 && (startTime == 0 || Utility::GetTime() - startTime < 30))
+ return;
+
+ std::sort(endpoints.begin(), endpoints.end(),
+ [](const ConfigObject::Ptr& a, const ConfigObject::Ptr& b) {
+ return a->GetName() < b->GetName();
+ }
+ );
+ }
+
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!dtype)
+ continue;
+
+ for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
+ if (!object->IsActive() || object->GetHAMode() != HARunOnce)
+ continue;
+
+ bool authority;
+
+ if (!my_zone)
+ authority = true;
+ else
+ authority = endpoints[Utility::SDBM(object->GetName()) % endpoints.size()] == my_endpoint;
+
+#ifdef I2_DEBUG
+// //Enable on demand, causes heavy logging on each run.
+// Log(LogDebug, "ApiListener")
+// << "Setting authority '" << Convert::ToString(authority) << "' for object '" << object->GetName() << "' of type '" << object->GetReflectionType()->GetName() << "'.";
+#endif /* I2_DEBUG */
+
+ object->SetAuthority(authority);
+ }
+ }
+
+ m_UpdatedObjectAuthority.store(true);
+}
diff --git a/lib/remote/apilistener-configsync.cpp b/lib/remote/apilistener-configsync.cpp
new file mode 100644
index 0000000..a12db0b
--- /dev/null
+++ b/lib/remote/apilistener-configsync.cpp
@@ -0,0 +1,464 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/configobjectutility.hpp"
+#include "remote/jsonrpc.hpp"
+#include "base/configtype.hpp"
+#include "base/json.hpp"
+#include "base/convert.hpp"
+#include "config/vmops.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+REGISTER_APIFUNCTION(UpdateObject, config, &ApiListener::ConfigUpdateObjectAPIHandler);
+REGISTER_APIFUNCTION(DeleteObject, config, &ApiListener::ConfigDeleteObjectAPIHandler);
+
+INITIALIZE_ONCE([]() {
+ ConfigObject::OnActiveChanged.connect(&ApiListener::ConfigUpdateObjectHandler);
+ ConfigObject::OnVersionChanged.connect(&ApiListener::ConfigUpdateObjectHandler);
+});
+
+void ApiListener::ConfigUpdateObjectHandler(const ConfigObject::Ptr& object, const Value& cookie)
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ if (object->IsActive()) {
+ /* Sync object config */
+ listener->UpdateConfigObject(object, cookie);
+ } else if (!object->IsActive() && object->GetExtension("ConfigObjectDeleted")) {
+ /* Delete object */
+ listener->DeleteConfigObject(object, cookie);
+ }
+}
+
+Value ApiListener::ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Log(LogNotice, "ApiListener")
+ << "Received config update for object: " << JsonEncode(params);
+
+ /* check permissions */
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ String objType = params->Get("type");
+ String objName = params->Get("name");
+
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ String identity = origin->FromClient->GetIdentity();
+
+ /* discard messages if the client is not configured on this node */
+ if (!endpoint) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config update object' message from '" << identity << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Zone::Ptr endpointZone = endpoint->GetZone();
+
+ /* discard messages if the sender is in a child zone */
+ if (!Zone::GetLocalZone()->IsChildOf(endpointZone)) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config update object' message"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. Sender is in a child zone.";
+ return Empty;
+ }
+
+ String objZone = params->Get("zone");
+
+ if (!objZone.IsEmpty() && !Zone::GetByName(objZone)) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config update object' message"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. Objects zone '" << objZone << "' isn't known locally.";
+ return Empty;
+ }
+
+ /* ignore messages if the endpoint does not accept config */
+ if (!listener->GetAcceptConfig()) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring config update"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. '" << listener->GetName() << "' does not accept config.";
+ return Empty;
+ }
+
+ /* update the object */
+ double objVersion = params->Get("version");
+
+ Type::Ptr ptype = Type::GetByName(objType);
+ auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
+
+ if (!ctype) {
+ // This never happens with icinga cluster endpoints, only with development errors.
+ Log(LogCritical, "ApiListener")
+ << "Config type '" << objType << "' does not exist.";
+ return Empty;
+ }
+
+ ConfigObject::Ptr object = ctype->GetObject(objName);
+
+ String config = params->Get("config");
+
+ bool newObject = false;
+
+ if (!object && !config.IsEmpty()) {
+ newObject = true;
+
+ /* object does not exist, create it through the API */
+ Array::Ptr errors = new Array();
+
+ /*
+ * Create the config object through our internal API.
+ * IMPORTANT: Pass the origin to prevent cluster sync loops.
+ */
+ if (!ConfigObjectUtility::CreateObject(ptype, objName, config, errors, nullptr, origin)) {
+ Log(LogCritical, "ApiListener")
+ << "Could not create object '" << objName << "':";
+
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "ApiListener", error);
+ }
+
+ return Empty;
+ }
+
+ object = ctype->GetObject(objName);
+
+ if (!object)
+ return Empty;
+
+ /* object was created, update its version */
+ object->SetVersion(objVersion, false, origin);
+ }
+
+ if (!object)
+ return Empty;
+
+ /* update object attributes if version was changed or if this is a new object */
+ if (newObject || objVersion <= object->GetVersion()) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding config update"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << object->GetName()
+ << "': Object version " << std::fixed << object->GetVersion()
+ << " is more recent than the received version " << std::fixed << objVersion << ".";
+
+ return Empty;
+ }
+
+ Log(LogNotice, "ApiListener")
+ << "Processing config update"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << object->GetName()
+ << "': Object version " << object->GetVersion()
+ << " is older than the received version " << objVersion << ".";
+
+ Dictionary::Ptr modified_attributes = params->Get("modified_attributes");
+
+ if (modified_attributes) {
+ ObjectLock olock(modified_attributes);
+ for (const Dictionary::Pair& kv : modified_attributes) {
+ /* update all modified attributes
+ * but do not update the object version yet.
+ * This triggers cluster events otherwise.
+ */
+ object->ModifyAttribute(kv.first, kv.second, false);
+ }
+ }
+
+ /* check whether original attributes changed and restore them locally */
+ Array::Ptr newOriginalAttributes = params->Get("original_attributes");
+ Dictionary::Ptr objOriginalAttributes = object->GetOriginalAttributes();
+
+ if (newOriginalAttributes && objOriginalAttributes) {
+ std::vector<String> restoreAttrs;
+
+ {
+ ObjectLock xlock(objOriginalAttributes);
+ for (const Dictionary::Pair& kv : objOriginalAttributes) {
+ /* original attribute was removed, restore it */
+ if (!newOriginalAttributes->Contains(kv.first))
+ restoreAttrs.push_back(kv.first);
+ }
+ }
+
+ for (const String& key : restoreAttrs) {
+ /* do not update the object version yet. */
+ object->RestoreAttribute(key, false);
+ }
+ }
+
+ /* keep the object version in sync with the sender */
+ object->SetVersion(objVersion, false, origin);
+
+ return Empty;
+}
+
+Value ApiListener::ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ Log(LogNotice, "ApiListener")
+ << "Received config delete for object: " << JsonEncode(params);
+
+ /* check permissions */
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ String objType = params->Get("type");
+ String objName = params->Get("name");
+
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ String identity = origin->FromClient->GetIdentity();
+
+ if (!endpoint) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config delete object' message from '" << identity << "': Invalid endpoint origin (client not allowed).";
+ return Empty;
+ }
+
+ Zone::Ptr endpointZone = endpoint->GetZone();
+
+ /* discard messages if the sender is in a child zone */
+ if (!Zone::GetLocalZone()->IsChildOf(endpointZone)) {
+ Log(LogNotice, "ApiListener")
+ << "Discarding 'config delete object' message"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. Sender is in a child zone.";
+ return Empty;
+ }
+
+ if (!listener->GetAcceptConfig()) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring config delete"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << objName << "' of type '" << objType << "'. '" << listener->GetName() << "' does not accept config.";
+ return Empty;
+ }
+
+ /* delete the object */
+ Type::Ptr ptype = Type::GetByName(objType);
+ auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
+
+ if (!ctype) {
+ // This never happens with icinga cluster endpoints, only with development errors.
+ Log(LogCritical, "ApiListener")
+ << "Config type '" << objType << "' does not exist.";
+ return Empty;
+ }
+
+ ConfigObject::Ptr object = ctype->GetObject(objName);
+
+ if (!object) {
+ Log(LogNotice, "ApiListener")
+ << "Could not delete non-existent object '" << objName << "' with type '" << params->Get("type") << "'.";
+ return Empty;
+ }
+
+ if (object->GetPackage() != "_api") {
+ Log(LogCritical, "ApiListener")
+ << "Could not delete object '" << objName << "': Not created by the API.";
+ return Empty;
+ }
+
+ Log(LogNotice, "ApiListener")
+ << "Processing config delete"
+ << " from '" << identity << "' (endpoint: '" << endpoint->GetName() << "', zone: '" << endpointZone->GetName() << "')"
+ << " for object '" << object->GetName() << "'.";
+
+ Array::Ptr errors = new Array();
+
+ /*
+ * Delete the config object through our internal API.
+ * IMPORTANT: Pass the origin to prevent cluster sync loops.
+ */
+ if (!ConfigObjectUtility::DeleteObject(object, true, errors, nullptr, origin)) {
+ Log(LogCritical, "ApiListener", "Could not delete object:");
+
+ ObjectLock olock(errors);
+ for (const String& error : errors) {
+ Log(LogCritical, "ApiListener", error);
+ }
+ }
+
+ return Empty;
+}
+
+void ApiListener::UpdateConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
+ const JsonRpcConnection::Ptr& client)
+{
+ /* only send objects to zones which have access to the object */
+ if (client) {
+ Zone::Ptr target_zone = client->GetEndpoint()->GetZone();
+
+ if (target_zone && !target_zone->CanAccessObject(object)) {
+ Log(LogDebug, "ApiListener")
+ << "Not sending 'update config' message to unauthorized zone '" << target_zone->GetName() << "'"
+ << " for object: '" << object->GetName() << "'.";
+
+ return;
+ }
+ }
+
+ if (object->GetPackage() != "_api" && object->GetVersion() == 0)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+
+ Dictionary::Ptr message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "config::UpdateObject" },
+ { "params", params }
+ });
+
+ params->Set("name", object->GetName());
+ params->Set("type", object->GetReflectionType()->GetName());
+ params->Set("version", object->GetVersion());
+
+ String zoneName = object->GetZoneName();
+
+ if (!zoneName.IsEmpty())
+ params->Set("zone", zoneName);
+
+ if (object->GetPackage() == "_api") {
+ std::ifstream fp(ConfigObjectUtility::GetExistingObjectConfigPath(object).CStr(), std::ifstream::binary);
+ if (!fp)
+ return;
+
+ String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+ params->Set("config", content);
+ }
+
+ Dictionary::Ptr original_attributes = object->GetOriginalAttributes();
+ Dictionary::Ptr modified_attributes = new Dictionary();
+ ArrayData newOriginalAttributes;
+
+ if (original_attributes) {
+ ObjectLock olock(original_attributes);
+ for (const Dictionary::Pair& kv : original_attributes) {
+ std::vector<String> tokens = kv.first.Split(".");
+
+ Value value = object;
+ for (const String& token : tokens) {
+ value = VMOps::GetField(value, token);
+ }
+
+ modified_attributes->Set(kv.first, value);
+
+ newOriginalAttributes.push_back(kv.first);
+ }
+ }
+
+ params->Set("modified_attributes", modified_attributes);
+
+ /* only send the original attribute keys */
+ params->Set("original_attributes", new Array(std::move(newOriginalAttributes)));
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "ApiListener")
+ << "Sent update for object '" << object->GetName() << "': " << JsonEncode(params);
+#endif /* I2_DEBUG */
+
+ if (client)
+ client->SendMessage(message);
+ else {
+ Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone());
+
+ if (!target)
+ target = Zone::GetLocalZone();
+
+ RelayMessage(origin, target, message, false);
+ }
+}
+
+
+void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
+ const JsonRpcConnection::Ptr& client)
+{
+ if (object->GetPackage() != "_api")
+ return;
+
+ /* only send objects to zones which have access to the object */
+ if (client) {
+ Zone::Ptr target_zone = client->GetEndpoint()->GetZone();
+
+ if (target_zone && !target_zone->CanAccessObject(object)) {
+ Log(LogDebug, "ApiListener")
+ << "Not sending 'delete config' message to unauthorized zone '" << target_zone->GetName() << "'"
+ << " for object: '" << object->GetName() << "'.";
+
+ return;
+ }
+ }
+
+ Dictionary::Ptr params = new Dictionary();
+
+ Dictionary::Ptr message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "config::DeleteObject" },
+ { "params", params }
+ });
+
+ params->Set("name", object->GetName());
+ params->Set("type", object->GetReflectionType()->GetName());
+ params->Set("version", object->GetVersion());
+
+
+#ifdef I2_DEBUG
+ Log(LogDebug, "ApiListener")
+ << "Sent delete for object '" << object->GetName() << "': " << JsonEncode(params);
+#endif /* I2_DEBUG */
+
+ if (client)
+ client->SendMessage(message);
+ else {
+ Zone::Ptr target = static_pointer_cast<Zone>(object->GetZone());
+
+ if (!target)
+ target = Zone::GetLocalZone();
+
+ RelayMessage(origin, target, message, true);
+ }
+}
+
+/* Initial sync on connect for new endpoints */
+void ApiListener::SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient)
+{
+ Endpoint::Ptr endpoint = aclient->GetEndpoint();
+ ASSERT(endpoint);
+
+ Zone::Ptr azone = endpoint->GetZone();
+
+ Log(LogInformation, "ApiListener")
+ << "Syncing runtime objects to endpoint '" << endpoint->GetName() << "'.";
+
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ auto *dtype = dynamic_cast<ConfigType *>(type.get());
+
+ if (!dtype)
+ continue;
+
+ for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
+ /* don't sync objects for non-matching parent-child zones */
+ if (!azone->CanAccessObject(object))
+ continue;
+
+ /* send the config object to the connected client */
+ UpdateConfigObject(object, nullptr, aclient);
+ }
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Finished syncing runtime objects to endpoint '" << endpoint->GetName() << "'.";
+}
diff --git a/lib/remote/apilistener-filesync.cpp b/lib/remote/apilistener-filesync.cpp
new file mode 100644
index 0000000..acf8deb
--- /dev/null
+++ b/lib/remote/apilistener-filesync.cpp
@@ -0,0 +1,887 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "config/configcompiler.hpp"
+#include "base/tlsutility.hpp"
+#include "base/json.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/application.hpp"
+#include "base/exception.hpp"
+#include "base/shared.hpp"
+#include "base/utility.hpp"
+#include <fstream>
+#include <iomanip>
+#include <thread>
+
+using namespace icinga;
+
+REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
+
+std::mutex ApiListener::m_ConfigSyncStageLock;
+
+/**
+ * Entrypoint for updating all authoritative configs from /etc/zones.d, packages, etc.
+ * into var/lib/icinga2/api/zones
+ */
+void ApiListener::SyncLocalZoneDirs() const
+{
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ try {
+ SyncLocalZoneDir(zone);
+ } catch (const std::exception&) {
+ continue;
+ }
+ }
+}
+
+/**
+ * Sync a zone directory where we have an authoritative copy (zones.d, packages, etc.)
+ *
+ * This function collects the registered zone config dirs from
+ * the config compiler and reads the file content into the config
+ * information structure.
+ *
+ * Returns early when there are no updates.
+ *
+ * @param zone Pointer to the zone object being synced.
+ */
+void ApiListener::SyncLocalZoneDir(const Zone::Ptr& zone) const
+{
+ if (!zone)
+ return;
+
+ ConfigDirInformation newConfigInfo;
+ newConfigInfo.UpdateV1 = new Dictionary();
+ newConfigInfo.UpdateV2 = new Dictionary();
+ newConfigInfo.Checksums = new Dictionary();
+
+ String zoneName = zone->GetName();
+
+ // Load registered zone paths, e.g. '_etc', '_api' and user packages.
+ for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) {
+ ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
+
+ // Config files '*.conf'.
+ {
+ ObjectLock olock(newConfigPart.UpdateV1);
+ for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
+ String path = "/" + zf.Tag + kv.first;
+
+ newConfigInfo.UpdateV1->Set(path, kv.second);
+ newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
+ }
+ }
+
+ // Meta files.
+ {
+ ObjectLock olock(newConfigPart.UpdateV2);
+ for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
+ String path = "/" + zf.Tag + kv.first;
+
+ newConfigInfo.UpdateV2->Set(path, kv.second);
+ newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
+ }
+ }
+ }
+
+ size_t sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
+
+ // Return early if there are no updates.
+ if (sumUpdates == 0)
+ return;
+
+ String productionZonesDir = GetApiZonesDir() + zoneName;
+
+ Log(LogInformation, "ApiListener")
+ << "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << productionZonesDir << "'.";
+
+ // Purge files to allow deletion via zones.d.
+ if (Utility::PathExists(productionZonesDir))
+ Utility::RemoveDirRecursive(productionZonesDir);
+
+ Utility::MkDirP(productionZonesDir, 0700);
+
+ // Copy content and add additional meta data.
+ size_t numBytes = 0;
+
+ /* Note: We cannot simply copy directories here.
+ *
+ * Zone directories are registered from everywhere and we already
+ * have read their content into memory with LoadConfigDir().
+ */
+ Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
+
+ {
+ ObjectLock olock(newConfig);
+
+ for (const Dictionary::Pair& kv : newConfig) {
+ String dst = productionZonesDir + "/" + kv.first;
+
+ Utility::MkDirP(Utility::DirName(dst), 0755);
+
+ Log(LogInformation, "ApiListener")
+ << "Updating configuration file: " << dst;
+
+ String content = kv.second;
+
+ std::ofstream fp(dst.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+
+ fp << content;
+ fp.close();
+
+ numBytes += content.GetLength();
+ }
+ }
+
+ // Additional metadata.
+ String tsPath = productionZonesDir + "/.timestamp";
+
+ if (!Utility::PathExists(tsPath)) {
+ std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
+
+ fp << std::fixed << Utility::GetTime();
+ fp.close();
+ }
+
+ String authPath = productionZonesDir + "/.authoritative";
+
+ if (!Utility::PathExists(authPath)) {
+ std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
+ fp.close();
+ }
+
+ // Checksums.
+ String checksumsPath = productionZonesDir + "/.checksums";
+
+ if (Utility::PathExists(checksumsPath))
+ Utility::Remove(checksumsPath);
+
+ std::ofstream fp(checksumsPath.CStr(), std::ofstream::out | std::ostream::trunc);
+
+ fp << std::fixed << JsonEncode(newConfigInfo.Checksums);
+ fp.close();
+
+ Log(LogNotice, "ApiListener")
+ << "Updated meta data for cluster config sync. Checksum: '" << checksumsPath
+ << "', timestamp: '" << tsPath << "', auth: '" << authPath << "'.";
+}
+
+/**
+ * Entrypoint for sending a file based config update to a cluster client.
+ * This includes security checks for zone relations.
+ * Loads the zone config files where this client belongs to
+ * and sends the 'config::Update' JSON-RPC message.
+ *
+ * @param aclient Connected JSON-RPC client.
+ */
+void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
+{
+ Endpoint::Ptr endpoint = aclient->GetEndpoint();
+ ASSERT(endpoint);
+
+ Zone::Ptr clientZone = endpoint->GetZone();
+ Zone::Ptr localZone = Zone::GetLocalZone();
+
+ // Don't send config updates to parent zones
+ if (!clientZone->IsChildOf(localZone))
+ return;
+
+ Dictionary::Ptr configUpdateV1 = new Dictionary();
+ Dictionary::Ptr configUpdateV2 = new Dictionary();
+ Dictionary::Ptr configUpdateChecksums = new Dictionary(); // new since 2.11
+
+ String zonesDir = GetApiZonesDir();
+
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ String zoneName = zone->GetName();
+ String zoneDir = zonesDir + zoneName;
+
+ // Only sync child and global zones.
+ if (!zone->IsChildOf(clientZone) && !zone->IsGlobal())
+ continue;
+
+ // Zone was configured, but there's no configuration directory.
+ if (!Utility::PathExists(zoneDir))
+ continue;
+
+ Log(LogInformation, "ApiListener")
+ << "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
+ << "zone '" << zoneName << "' to endpoint '" << endpoint->GetName() << "'.";
+
+ ConfigDirInformation config = LoadConfigDir(zoneDir);
+
+ configUpdateV1->Set(zoneName, config.UpdateV1);
+ configUpdateV2->Set(zoneName, config.UpdateV2);
+ configUpdateChecksums->Set(zoneName, config.Checksums); // new since 2.11
+ }
+
+ Dictionary::Ptr message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "config::Update" },
+ { "params", new Dictionary({
+ { "update", configUpdateV1 },
+ { "update_v2", configUpdateV2 }, // Since 2.4.2.
+ { "checksums", configUpdateChecksums } // Since 2.11.0.
+ }) }
+ });
+
+ aclient->SendMessage(message);
+}
+
+static bool CompareTimestampsConfigChange(const Dictionary::Ptr& productionConfig, const Dictionary::Ptr& receivedConfig,
+ const String& stageConfigZoneDir)
+{
+ double productionTimestamp;
+ double receivedTimestamp;
+
+ // Missing production timestamp means that something really broke. Always trigger a config change then.
+ if (!productionConfig->Contains("/.timestamp"))
+ productionTimestamp = 0;
+ else
+ productionTimestamp = productionConfig->Get("/.timestamp");
+
+ // Missing received config timestamp means that something really broke. Always trigger a config change then.
+ if (!receivedConfig->Contains("/.timestamp"))
+ receivedTimestamp = Utility::GetTime() + 10;
+ else
+ receivedTimestamp = receivedConfig->Get("/.timestamp");
+
+ bool configChange;
+
+ // Skip update if our configuration files are more recent.
+ if (productionTimestamp >= receivedTimestamp) {
+
+ Log(LogInformation, "ApiListener")
+ << "Our production configuration is more recent than the received configuration update."
+ << " Ignoring configuration file update for path '" << stageConfigZoneDir << "'. Current timestamp '"
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", productionTimestamp) << "' ("
+ << std::fixed << std::setprecision(6) << productionTimestamp
+ << ") >= received timestamp '"
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", receivedTimestamp) << "' ("
+ << receivedTimestamp << ").";
+
+ configChange = false;
+
+ } else {
+ configChange = true;
+ }
+
+ // Update the .timestamp file inside the staging directory.
+ String tsPath = stageConfigZoneDir + "/.timestamp";
+
+ if (!Utility::PathExists(tsPath)) {
+ std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
+ fp << std::fixed << receivedTimestamp;
+ fp.close();
+ }
+
+ return configChange;
+}
+
+/**
+ * Registered handler when a new config::Update message is received.
+ *
+ * Checks destination and permissions first, locks the transaction and analyses the update.
+ * The newly received configuration is not copied to production immediately,
+ * but into the staging directory first.
+ * Last, the async validation and restart is triggered.
+ *
+ * @param origin Where this message came from.
+ * @param params Message parameters including the config updates.
+ * @returns Empty, required by the interface.
+ */
+Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ // Verify permissions and trust relationship.
+ if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
+ return Empty;
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener) {
+ Log(LogCritical, "ApiListener", "No instance available.");
+ return Empty;
+ }
+
+ if (!listener->GetAcceptConfig()) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring config update. '" << listener->GetName() << "' does not accept config.";
+ return Empty;
+ }
+
+ std::thread([origin, params, listener]() {
+ try {
+ listener->HandleConfigUpdate(origin, params);
+ } catch (const std::exception& ex) {
+ auto msg ("Exception during config sync: " + DiagnosticInformation(ex));
+
+ Log(LogCritical, "ApiListener") << msg;
+ listener->UpdateLastFailedZonesStageValidation(msg);
+ }
+ }).detach();
+ return Empty;
+}
+
+void ApiListener::HandleConfigUpdate(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ /* Only one transaction is allowed, concurrent message handlers need to wait.
+ * This affects two parent endpoints sending the config in the same moment.
+ */
+ std::lock_guard<std::mutex> lock(m_ConfigSyncStageLock);
+
+ String apiZonesStageDir = GetApiZonesStageDir();
+ String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
+ String fromZoneName = GetFromZoneName(origin->FromZone);
+
+ Log(LogInformation, "ApiListener")
+ << "Applying config update from endpoint '" << fromEndpointName
+ << "' of zone '" << fromZoneName << "'.";
+
+ // Config files.
+ Dictionary::Ptr updateV1 = params->Get("update");
+ // Meta data files: .timestamp, etc.
+ Dictionary::Ptr updateV2 = params->Get("update_v2");
+
+ // New since 2.11.0.
+ Dictionary::Ptr checksums;
+
+ if (params->Contains("checksums"))
+ checksums = params->Get("checksums");
+
+ bool configChange = false;
+
+ // Keep track of the relative config paths for later validation and copying. TODO: Find a better algorithm.
+ std::vector<String> relativePaths;
+
+ /*
+ * We can and must safely purge the staging directory, as the difference is taken between
+ * runtime production config and newly received configuration.
+ * This is needed to not mix deleted/changed content between received and stage
+ * config.
+ */
+ if (Utility::PathExists(apiZonesStageDir))
+ Utility::RemoveDirRecursive(apiZonesStageDir);
+
+ Utility::MkDirP(apiZonesStageDir, 0700);
+
+ // Analyse and process the update.
+ size_t count = 0;
+
+ ObjectLock olock(updateV1);
+
+ for (const Dictionary::Pair& kv : updateV1) {
+
+ // Check for the configured zones.
+ String zoneName = kv.first;
+ Zone::Ptr zone = Zone::GetByName(zoneName);
+
+ if (!zone) {
+ Log(LogWarning, "ApiListener")
+ << "Ignoring config update from endpoint '" << fromEndpointName
+ << "' for unknown zone '" << zoneName << "'.";
+
+ continue;
+ }
+
+ // Ignore updates where we have an authoritive copy in etc/zones.d, packages, etc.
+ if (ConfigCompiler::HasZoneConfigAuthority(zoneName)) {
+ Log(LogInformation, "ApiListener")
+ << "Ignoring config update from endpoint '" << fromEndpointName
+ << "' for zone '" << zoneName << "' because we have an authoritative version of the zone's config.";
+
+ continue;
+ }
+
+ // Put the received configuration into our stage directory.
+ String productionConfigZoneDir = GetApiZonesDir() + zoneName;
+ String stageConfigZoneDir = GetApiZonesStageDir() + zoneName;
+
+ Utility::MkDirP(productionConfigZoneDir, 0700);
+ Utility::MkDirP(stageConfigZoneDir, 0700);
+
+ // Merge the config information.
+ ConfigDirInformation newConfigInfo;
+ newConfigInfo.UpdateV1 = kv.second;
+
+ // Load metadata.
+ if (updateV2)
+ newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
+
+ // Load checksums. New since 2.11.
+ if (checksums)
+ newConfigInfo.Checksums = checksums->Get(kv.first);
+
+ // Load the current production config details.
+ ConfigDirInformation productionConfigInfo = LoadConfigDir(productionConfigZoneDir);
+
+ // Merge updateV1 and updateV2
+ Dictionary::Ptr productionConfig = MergeConfigUpdate(productionConfigInfo);
+ Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
+
+ bool timestampChanged = false;
+
+ if (CompareTimestampsConfigChange(productionConfig, newConfig, stageConfigZoneDir)) {
+ timestampChanged = true;
+ }
+
+ /* If we have received 'checksums' via cluster message, go for it.
+ * Otherwise do the old timestamp dance for versions < 2.11.
+ */
+ if (checksums) {
+ Log(LogInformation, "ApiListener")
+ << "Received configuration for zone '" << zoneName << "' from endpoint '"
+ << fromEndpointName << "'. Comparing the timestamp and checksums.";
+
+ if (timestampChanged) {
+
+ if (CheckConfigChange(productionConfigInfo, newConfigInfo))
+ configChange = true;
+ }
+
+ } else {
+ /* Fallback to timestamp handling when the parent endpoint didn't send checks.
+ * This can happen when the satellite is 2.11 and the master is 2.10.
+ *
+ * TODO: Deprecate and remove this behaviour in 2.13+.
+ */
+
+ Log(LogWarning, "ApiListener")
+ << "Received configuration update without checksums from parent endpoint "
+ << fromEndpointName << ". This behaviour is deprecated. Please upgrade the parent endpoint to 2.11+";
+
+ if (timestampChanged) {
+ configChange = true;
+ }
+
+ // Keep another hack when there's a timestamp file missing.
+ {
+ ObjectLock olock(newConfig);
+
+ for (const Dictionary::Pair &kv : newConfig) {
+
+ // This is super expensive with a string content comparison.
+ if (productionConfig->Get(kv.first) != kv.second) {
+ if (!Utility::Match("*/.timestamp", kv.first))
+ configChange = true;
+ }
+ }
+ }
+ }
+
+ // Dump the received configuration for this zone into the stage directory.
+ size_t numBytes = 0;
+
+ {
+ ObjectLock olock(newConfig);
+
+ for (const Dictionary::Pair& kv : newConfig) {
+
+ /* Store the relative config file path for later validation and activation.
+ * IMPORTANT: Store this prior to any filters.
+ * */
+ relativePaths.push_back(zoneName + "/" + kv.first);
+
+ String path = stageConfigZoneDir + "/" + kv.first;
+
+ if (Utility::Match("*.conf", path)) {
+ Log(LogInformation, "ApiListener")
+ << "Stage: Updating received configuration file '" << path << "' for zone '" << zoneName << "'.";
+ }
+
+ // Parent nodes < 2.11 always send this, avoid this bug and deny its receival prior to writing it on disk.
+ if (Utility::BaseName(path) == ".authoritative")
+ continue;
+
+ // Sync string content only.
+ String content = kv.second;
+
+ // Generate a directory tree (zones/1/2/3 might not exist yet).
+ Utility::MkDirP(Utility::DirName(path), 0755);
+
+ // Write the content to file.
+ std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fp << content;
+ fp.close();
+
+ numBytes += content.GetLength();
+ }
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Applying configuration file update for path '" << stageConfigZoneDir << "' ("
+ << numBytes << " Bytes).";
+
+ if (timestampChanged) {
+ // If the update removes a path, delete it on disk and signal a config change.
+ ObjectLock xlock(productionConfig);
+
+ for (const Dictionary::Pair& kv : productionConfig) {
+ if (!newConfig->Contains(kv.first)) {
+ configChange = true;
+
+ String path = stageConfigZoneDir + "/" + kv.first;
+ Utility::Remove(path);
+ }
+ }
+ }
+
+ count++;
+ }
+
+ /*
+ * We have processed all configuration files and stored them in the staging directory.
+ *
+ * We need to store them locally for later analysis. A config change means
+ * that we will validate the configuration in a separate process sandbox,
+ * and only copy the configuration to production when everything is ok.
+ *
+ * A successful validation also triggers the final restart.
+ */
+ if (configChange) {
+ Log(LogInformation, "ApiListener")
+ << "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
+ << "' are different to production, triggering validation and reload.";
+ TryActivateZonesStage(relativePaths);
+ } else {
+ Log(LogInformation, "ApiListener")
+ << "Received configuration updates (" << count << ") from endpoint '" << fromEndpointName
+ << "' are equal to production, skipping validation and reload.";
+ ClearLastFailedZonesStageValidation();
+ }
+}
+
+/**
+ * Spawns a new validation process with 'Internal.ZonesStageVarDir' set to override the config validation zone dirs with
+ * our current stage. Then waits for the validation result and if it was successful, the configuration is copied from
+ * stage to production and a restart is triggered. On validation failure, there is no restart and this is logged.
+ *
+ * The caller of this function must hold m_ConfigSyncStageLock.
+ *
+ * @param relativePaths Collected paths including the zone name, which are copied from stage to current directories.
+ */
+void ApiListener::TryActivateZonesStage(const std::vector<String>& relativePaths)
+{
+ VERIFY(Application::GetArgC() >= 1);
+
+ /* Inherit parent process args. */
+ Array::Ptr args = new Array({
+ Application::GetExePath(Application::GetArgV()[0]),
+ });
+
+ for (int i = 1; i < Application::GetArgC(); i++) {
+ String argV = Application::GetArgV()[i];
+
+ if (argV == "-d" || argV == "--daemonize")
+ continue;
+
+ args->Add(argV);
+ }
+
+ args->Add("--validate");
+
+ // Set the ZonesStageDir. This creates our own local chroot without any additional automated zone includes.
+ args->Add("--define");
+ args->Add("Internal.ZonesStageVarDir=" + GetApiZonesStageDir());
+
+ Process::Ptr process = new Process(Process::PrepareCommand(args));
+ process->SetTimeout(Application::GetReloadTimeout());
+
+ process->Run();
+ const ProcessResult& pr = process->WaitForResult();
+
+ String apiDir = GetApiDir();
+ String apiZonesDir = GetApiZonesDir();
+ String apiZonesStageDir = GetApiZonesStageDir();
+
+ String logFile = apiDir + "/zones-stage-startup.log";
+ std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpLog << pr.Output;
+ fpLog.close();
+
+ String statusFile = apiDir + "/zones-stage-status";
+ std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpStatus << pr.ExitStatus;
+ fpStatus.close();
+
+ // Validation went fine, copy stage and reload.
+ if (pr.ExitStatus == 0) {
+ Log(LogInformation, "ApiListener")
+ << "Config validation for stage '" << apiZonesStageDir << "' was OK, replacing into '" << apiZonesDir << "' and triggering reload.";
+
+ // Purge production before copying stage.
+ if (Utility::PathExists(apiZonesDir))
+ Utility::RemoveDirRecursive(apiZonesDir);
+
+ Utility::MkDirP(apiZonesDir, 0700);
+
+ // Copy all synced configuration files from stage to production.
+ for (const String& path : relativePaths) {
+ if (!Utility::PathExists(apiZonesStageDir + path))
+ continue;
+
+ Log(LogInformation, "ApiListener")
+ << "Copying file '" << path << "' from config sync staging to production zones directory.";
+
+ String stagePath = apiZonesStageDir + path;
+ String currentPath = apiZonesDir + path;
+
+ Utility::MkDirP(Utility::DirName(currentPath), 0700);
+
+ Utility::CopyFile(stagePath, currentPath);
+ }
+
+ // Clear any failed deployment before
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener)
+ listener->ClearLastFailedZonesStageValidation();
+
+ Application::RequestRestart();
+
+ // All good, return early.
+ return;
+ }
+
+ String failedLogFile = apiDir + "/zones-stage-startup-last-failed.log";
+ std::ofstream fpFailedLog(failedLogFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpFailedLog << pr.Output;
+ fpFailedLog.close();
+
+ // Error case.
+ Log(LogCritical, "ApiListener")
+ << "Config validation failed for staged cluster config sync in '" << apiZonesStageDir
+ << "'. Aborting. Logs: '" << failedLogFile << "'";
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (listener)
+ listener->UpdateLastFailedZonesStageValidation(pr.Output);
+}
+
+/**
+ * Update the structure from the last failed validation output.
+ * Uses the current timestamp.
+ *
+ * @param log The process output from the config validation.
+ */
+void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
+{
+ Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
+ { "log", log },
+ { "ts", Utility::GetTime() }
+ });
+
+ SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
+}
+
+/**
+ * Clear the structure for the last failed reload.
+ *
+ */
+void ApiListener::ClearLastFailedZonesStageValidation()
+{
+ SetLastFailedZonesStageValidation(Dictionary::Ptr());
+}
+
+/**
+ * Generate a config checksum.
+ *
+ * @param content String content used for generating the checksum.
+ * @returns The checksum as string.
+ */
+String ApiListener::GetChecksum(const String& content)
+{
+ return SHA256(content);
+}
+
+bool ApiListener::CheckConfigChange(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig)
+{
+ Dictionary::Ptr oldChecksums = oldConfig.Checksums;
+ Dictionary::Ptr newChecksums = newConfig.Checksums;
+
+ // TODO: Figure out whether normal users need this for debugging.
+ Log(LogDebug, "ApiListener")
+ << "Checking for config change between stage and production. Old (" << oldChecksums->GetLength() << "): '"
+ << JsonEncode(oldChecksums)
+ << "' vs. new (" << newChecksums->GetLength() << "): '"
+ << JsonEncode(newChecksums) << "'.";
+
+ /* Since internal files are synced here too, we can not depend on length.
+ * So we need to go through both checksum sets to cover the cases"everything is new" and "everything was deleted".
+ */
+ {
+ ObjectLock olock(oldChecksums);
+ for (const Dictionary::Pair& kv : oldChecksums) {
+ String path = kv.first;
+ String oldChecksum = kv.second;
+
+ /* Ignore internal files, especially .timestamp and .checksums.
+ *
+ * If we don't, this results in "always change" restart loops.
+ */
+ if (Utility::Match("/.*", path)) {
+ Log(LogDebug, "ApiListener")
+ << "Ignoring old internal file '" << path << "'.";
+
+ continue;
+ }
+
+ Log(LogDebug, "ApiListener")
+ << "Checking " << path << " for old checksum: " << oldChecksum << ".";
+
+ // Check if key exists first for more verbose logging.
+ // Note: Don't do this later on.
+ if (!newChecksums->Contains(path)) {
+ Log(LogDebug, "ApiListener")
+ << "File '" << path << "' was deleted by remote.";
+
+ return true;
+ }
+
+ String newChecksum = newChecksums->Get(path);
+
+ if (newChecksum != kv.second) {
+ Log(LogDebug, "ApiListener")
+ << "Path '" << path << "' doesn't match old checksum '"
+ << oldChecksum << "' with new checksum '" << newChecksum << "'.";
+
+ return true;
+ }
+ }
+ }
+
+ {
+ ObjectLock olock(newChecksums);
+ for (const Dictionary::Pair& kv : newChecksums) {
+ String path = kv.first;
+ String newChecksum = kv.second;
+
+ /* Ignore internal files, especially .timestamp and .checksums.
+ *
+ * If we don't, this results in "always change" restart loops.
+ */
+ if (Utility::Match("/.*", path)) {
+ Log(LogDebug, "ApiListener")
+ << "Ignoring new internal file '" << path << "'.";
+
+ continue;
+ }
+
+ Log(LogDebug, "ApiListener")
+ << "Checking " << path << " for new checksum: " << newChecksum << ".";
+
+ // Check if the checksum exists, checksums in both sets have already been compared
+ if (!oldChecksums->Contains(path)) {
+ Log(LogDebug, "ApiListener")
+ << "File '" << path << "' was added by remote.";
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Load the given config dir and read their file content into the config structure.
+ *
+ * @param dir Path to the config directory.
+ * @returns ConfigDirInformation structure.
+ */
+ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
+{
+ ConfigDirInformation config;
+ config.UpdateV1 = new Dictionary();
+ config.UpdateV2 = new Dictionary();
+ config.Checksums = new Dictionary();
+
+ Utility::GlobRecursive(dir, "*", [&config, dir](const String& file) { ConfigGlobHandler(config, dir, file); }, GlobFile);
+ return config;
+}
+
+/**
+ * Read the given file and store it in the config information structure.
+ * Callback function for Glob().
+ *
+ * @param config Reference to the config information object.
+ * @param path File path.
+ * @param file Full file name.
+ */
+void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
+{
+ // Avoid loading the authoritative marker for syncs at all cost.
+ if (Utility::BaseName(file) == ".authoritative")
+ return;
+
+ CONTEXT("Creating config update for file '" << file << "'");
+
+ Log(LogNotice, "ApiListener")
+ << "Creating config update for file '" << file << "'.";
+
+ std::ifstream fp(file.CStr(), std::ifstream::binary);
+ if (!fp)
+ return;
+
+ String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+
+ Dictionary::Ptr update;
+ String relativePath = file.SubStr(path.GetLength());
+
+ /*
+ * 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp).
+ *
+ * **Keep this intact to stay compatible with older clients.**
+ */
+ String sanitizedContent = Utility::ValidateUTF8(content);
+
+ if (Utility::Match("*.conf", file)) {
+ update = config.UpdateV1;
+
+ // Configuration files should be automatically sanitized with UTF8.
+ update->Set(relativePath, sanitizedContent);
+ } else {
+ update = config.UpdateV2;
+
+ /*
+ * Ensure that only valid UTF8 content is being read for the cluster config sync.
+ * Binary files are not supported when wrapped into JSON encoded messages.
+ * Rationale: https://github.com/Icinga/icinga2/issues/7382
+ */
+ if (content != sanitizedContent) {
+ Log(LogCritical, "ApiListener")
+ << "Ignoring file '" << file << "' for cluster config sync: Does not contain valid UTF8. Binary files are not supported.";
+ return;
+ }
+
+ update->Set(relativePath, content);
+ }
+
+ /* Calculate a checksum for each file (and a global one later).
+ *
+ * IMPORTANT: Ignore the .authoritative file above, this must not be synced.
+ * */
+ config.Checksums->Set(relativePath, GetChecksum(content));
+}
+
+/**
+ * Compatibility helper for merging config update v1 and v2 into a global result.
+ *
+ * @param config Config information structure.
+ * @returns Dictionary which holds the merged information.
+ */
+Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
+{
+ Dictionary::Ptr result = new Dictionary();
+
+ if (config.UpdateV1)
+ config.UpdateV1->CopyTo(result);
+
+ if (config.UpdateV2)
+ config.UpdateV2->CopyTo(result);
+
+ return result;
+}
diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp
new file mode 100644
index 0000000..85443e2
--- /dev/null
+++ b/lib/remote/apilistener.cpp
@@ -0,0 +1,1970 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apilistener.hpp"
+#include "remote/apilistener-ti.cpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/jsonrpc.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/configobjectutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/convert.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/netstring.hpp"
+#include "base/json.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/stdiostream.hpp"
+#include "base/perfdatavalue.hpp"
+#include "base/application.hpp"
+#include "base/context.hpp"
+#include "base/statsfunction.hpp"
+#include "base/exception.hpp"
+#include "base/tcpsocket.hpp"
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/io_context_strand.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/regex.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/thread/locks.hpp>
+#include <climits>
+#include <cstdint>
+#include <fstream>
+#include <memory>
+#include <openssl/ssl.h>
+#include <openssl/tls1.h>
+#include <openssl/x509.h>
+#include <sstream>
+#include <utility>
+
+using namespace icinga;
+
+REGISTER_TYPE(ApiListener);
+
+boost::signals2::signal<void(bool)> ApiListener::OnMasterChanged;
+ApiListener::Ptr ApiListener::m_Instance;
+
+REGISTER_STATSFUNCTION(ApiListener, &ApiListener::StatsFunc);
+
+REGISTER_APIFUNCTION(Hello, icinga, &ApiListener::HelloAPIHandler);
+
+ApiListener::ApiListener()
+{
+ m_RelayQueue.SetName("ApiListener, RelayQueue");
+ m_SyncQueue.SetName("ApiListener, SyncQueue");
+}
+
+String ApiListener::GetApiDir()
+{
+ return Configuration::DataDir + "/api/";
+}
+
+String ApiListener::GetApiZonesDir()
+{
+ return GetApiDir() + "zones/";
+}
+
+String ApiListener::GetApiZonesStageDir()
+{
+ return GetApiDir() + "zones-stage/";
+}
+
+String ApiListener::GetCertsDir()
+{
+ return Configuration::DataDir + "/certs/";
+}
+
+String ApiListener::GetCaDir()
+{
+ return Configuration::DataDir + "/ca/";
+}
+
+String ApiListener::GetCertificateRequestsDir()
+{
+ return Configuration::DataDir + "/certificate-requests/";
+}
+
+String ApiListener::GetDefaultCertPath()
+{
+ return GetCertsDir() + "/" + ScriptGlobal::Get("NodeName") + ".crt";
+}
+
+String ApiListener::GetDefaultKeyPath()
+{
+ return GetCertsDir() + "/" + ScriptGlobal::Get("NodeName") + ".key";
+}
+
+String ApiListener::GetDefaultCaPath()
+{
+ return GetCertsDir() + "/ca.crt";
+}
+
+double ApiListener::GetTlsHandshakeTimeout() const
+{
+ return Configuration::TlsHandshakeTimeout;
+}
+
+void ApiListener::SetTlsHandshakeTimeout(double value, bool suppress_events, const Value& cookie)
+{
+ Configuration::TlsHandshakeTimeout = value;
+}
+
+void ApiListener::CopyCertificateFile(const String& oldCertPath, const String& newCertPath)
+{
+ struct stat st1, st2;
+
+ if (!oldCertPath.IsEmpty() && stat(oldCertPath.CStr(), &st1) >= 0 && (stat(newCertPath.CStr(), &st2) < 0 || st1.st_mtime > st2.st_mtime)) {
+ Log(LogWarning, "ApiListener")
+ << "Copying '" << oldCertPath << "' certificate file to '" << newCertPath << "'";
+
+ Utility::MkDirP(Utility::DirName(newCertPath), 0700);
+ Utility::CopyFile(oldCertPath, newCertPath);
+ }
+}
+
+void ApiListener::OnConfigLoaded()
+{
+ if (m_Instance)
+ BOOST_THROW_EXCEPTION(ScriptError("Only one ApiListener object is allowed.", GetDebugInfo()));
+
+ m_Instance = this;
+
+ String defaultCertPath = GetDefaultCertPath();
+ String defaultKeyPath = GetDefaultKeyPath();
+ String defaultCaPath = GetDefaultCaPath();
+
+ /* Migrate certificate location < 2.8 to the new default path. */
+ String oldCertPath = GetCertPath();
+ String oldKeyPath = GetKeyPath();
+ String oldCaPath = GetCaPath();
+
+ CopyCertificateFile(oldCertPath, defaultCertPath);
+ CopyCertificateFile(oldKeyPath, defaultKeyPath);
+ CopyCertificateFile(oldCaPath, defaultCaPath);
+
+ if (!oldCertPath.IsEmpty() && !oldKeyPath.IsEmpty() && !oldCaPath.IsEmpty()) {
+ Log(LogWarning, "ApiListener", "Please read the upgrading documentation for v2.8: https://icinga.com/docs/icinga2/latest/doc/16-upgrading-icinga-2/");
+ }
+
+ /* Create the internal API object storage. */
+ ConfigObjectUtility::CreateStorage();
+
+ /* Cache API packages and their active stage name. */
+ UpdateActivePackageStagesCache();
+
+ /* set up SSL context */
+ std::shared_ptr<X509> cert;
+ try {
+ cert = GetX509Certificate(defaultCertPath);
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot get certificate from cert path: '"
+ + defaultCertPath + "'.", GetDebugInfo()));
+ }
+
+ try {
+ SetIdentity(GetCertificateCN(cert));
+ } catch (const std::exception&) {
+ BOOST_THROW_EXCEPTION(ScriptError("Cannot get certificate common name from cert path: '"
+ + defaultCertPath + "'.", GetDebugInfo()));
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "My API identity: " << GetIdentity();
+
+ UpdateSSLContext();
+}
+
+std::shared_ptr<X509> ApiListener::RenewCert(const std::shared_ptr<X509>& cert, bool ca)
+{
+ std::shared_ptr<EVP_PKEY> pubkey (X509_get_pubkey(cert.get()), EVP_PKEY_free);
+ auto subject (X509_get_subject_name(cert.get()));
+ auto cacert (GetX509Certificate(GetDefaultCaPath()));
+ auto newcert (CreateCertIcingaCA(pubkey.get(), subject, ca));
+
+ /* verify that the new cert matches the CA we're using for the ApiListener;
+ * this ensures that the CA we have in /var/lib/icinga2/ca matches the one
+ * we're using for cluster connections (there's no point in sending a client
+ * a certificate it wouldn't be able to use to connect to us anyway) */
+ try {
+ if (!VerifyCertificate(cacert, newcert, GetCrlPath())) {
+ Log(LogWarning, "ApiListener")
+ << "The CA in '" << GetDefaultCaPath() << "' does not match the CA which Icinga uses "
+ << "for its own cluster connections. This is most likely a configuration problem.";
+
+ return nullptr;
+ }
+ } catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */
+
+ return newcert;
+}
+
+void ApiListener::UpdateSSLContext()
+{
+ auto ctx (SetupSslContext(GetDefaultCertPath(), GetDefaultKeyPath(), GetDefaultCaPath(), GetCrlPath(), GetCipherList(), GetTlsProtocolmin(), GetDebugInfo()));
+
+ {
+ boost::unique_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
+
+ m_SSLContext = std::move(ctx);
+ }
+
+ for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ client->Disconnect();
+ }
+ }
+
+ for (const JsonRpcConnection::Ptr& client : m_AnonymousClients) {
+ client->Disconnect();
+ }
+}
+
+void ApiListener::OnAllConfigLoaded()
+{
+ m_LocalEndpoint = Endpoint::GetByName(GetIdentity());
+
+ if (!m_LocalEndpoint)
+ BOOST_THROW_EXCEPTION(ScriptError("Endpoint object for '" + GetIdentity() + "' is missing.", GetDebugInfo()));
+}
+
+/**
+ * Starts the component.
+ */
+void ApiListener::Start(bool runtimeCreated)
+{
+ Log(LogInformation, "ApiListener")
+ << "'" << GetName() << "' started.";
+
+ SyncLocalZoneDirs();
+
+ m_RenewOwnCertTimer = Timer::Create();
+
+ if (Utility::PathExists(GetIcingaCADir() + "/ca.key")) {
+ RenewOwnCert();
+ RenewCA();
+
+ m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) {
+ RenewOwnCert();
+ RenewCA();
+ });
+ } else {
+ m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) {
+ JsonRpcConnection::SendCertificateRequest(nullptr, nullptr, String());
+ });
+ }
+
+ m_RenewOwnCertTimer->SetInterval(RENEW_INTERVAL);
+ m_RenewOwnCertTimer->Start();
+
+ ObjectImpl<ApiListener>::Start(runtimeCreated);
+
+ {
+ std::unique_lock<std::mutex> lock(m_LogLock);
+ OpenLogFile();
+ }
+
+ /* create the primary JSON-RPC listener */
+ if (!AddListener(GetBindHost(), GetBindPort())) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot add listener on host '" << GetBindHost() << "' for port '" << GetBindPort() << "'.";
+ Application::Exit(EXIT_FAILURE);
+ }
+
+ m_Timer = Timer::Create();
+ m_Timer->OnTimerExpired.connect([this](const Timer * const&) { ApiTimerHandler(); });
+ m_Timer->SetInterval(5);
+ m_Timer->Start();
+ m_Timer->Reschedule(0);
+
+ m_ReconnectTimer = Timer::Create();
+ m_ReconnectTimer->OnTimerExpired.connect([this](const Timer * const&) { ApiReconnectTimerHandler(); });
+ m_ReconnectTimer->SetInterval(10);
+ m_ReconnectTimer->Start();
+ m_ReconnectTimer->Reschedule(0);
+
+ /* Keep this in relative sync with the cold startup in UpdateObjectAuthority() and the reconnect interval above.
+ * Previous: 60s reconnect, 30s OA, 60s cold startup.
+ * Now: 10s reconnect, 10s OA, 30s cold startup.
+ */
+ m_AuthorityTimer = Timer::Create();
+ m_AuthorityTimer->OnTimerExpired.connect([](const Timer * const&) { UpdateObjectAuthority(); });
+ m_AuthorityTimer->SetInterval(10);
+ m_AuthorityTimer->Start();
+
+ m_CleanupCertificateRequestsTimer = Timer::Create();
+ m_CleanupCertificateRequestsTimer->OnTimerExpired.connect([this](const Timer * const&) { CleanupCertificateRequestsTimerHandler(); });
+ m_CleanupCertificateRequestsTimer->SetInterval(3600);
+ m_CleanupCertificateRequestsTimer->Start();
+ m_CleanupCertificateRequestsTimer->Reschedule(0);
+
+ m_ApiPackageIntegrityTimer = Timer::Create();
+ m_ApiPackageIntegrityTimer->OnTimerExpired.connect([this](const Timer * const&) { CheckApiPackageIntegrity(); });
+ m_ApiPackageIntegrityTimer->SetInterval(300);
+ m_ApiPackageIntegrityTimer->Start();
+
+ OnMasterChanged(true);
+}
+
+void ApiListener::RenewOwnCert()
+{
+ auto certPath (GetDefaultCertPath());
+ auto cert (GetX509Certificate(certPath));
+
+ if (IsCertUptodate(cert)) {
+ return;
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Our certificate will expire soon, but we own the CA. Renewing.";
+
+ cert = RenewCert(cert);
+
+ if (!cert) {
+ return;
+ }
+
+ AtomicFile::Write(certPath, 0644, CertificateToString(cert));
+ UpdateSSLContext();
+}
+
+void ApiListener::RenewCA()
+{
+ auto certPath (GetCaDir() + "/ca.crt");
+ auto cert (GetX509Certificate(certPath));
+
+ if (IsCaUptodate(cert.get())) {
+ return;
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Our CA will expire soon, but we own it. Renewing.";
+
+ cert = RenewCert(cert, true);
+
+ if (!cert) {
+ return;
+ }
+
+ auto certStr (CertificateToString(cert));
+
+ AtomicFile::Write(GetDefaultCaPath(), 0644, certStr);
+ AtomicFile::Write(certPath, 0644, certStr);
+ UpdateSSLContext();
+}
+
+void ApiListener::Stop(bool runtimeDeleted)
+{
+ m_ApiPackageIntegrityTimer->Stop(true);
+ m_CleanupCertificateRequestsTimer->Stop(true);
+ m_AuthorityTimer->Stop(true);
+ m_ReconnectTimer->Stop(true);
+ m_Timer->Stop(true);
+ m_RenewOwnCertTimer->Stop(true);
+
+ ObjectImpl<ApiListener>::Stop(runtimeDeleted);
+
+ Log(LogInformation, "ApiListener")
+ << "'" << GetName() << "' stopped.";
+
+ {
+ std::unique_lock<std::mutex> lock(m_LogLock);
+ CloseLogFile();
+ RotateLogFile();
+ }
+
+ RemoveStatusFile();
+}
+
+ApiListener::Ptr ApiListener::GetInstance()
+{
+ return m_Instance;
+}
+
+Endpoint::Ptr ApiListener::GetMaster() const
+{
+ Zone::Ptr zone = Zone::GetLocalZone();
+
+ if (!zone)
+ return nullptr;
+
+ std::vector<String> names;
+
+ for (const Endpoint::Ptr& endpoint : zone->GetEndpoints())
+ if (endpoint->GetConnected() || endpoint->GetName() == GetIdentity())
+ names.push_back(endpoint->GetName());
+
+ std::sort(names.begin(), names.end());
+
+ return Endpoint::GetByName(*names.begin());
+}
+
+bool ApiListener::IsMaster() const
+{
+ Endpoint::Ptr master = GetMaster();
+
+ if (!master)
+ return false;
+
+ return master == GetLocalEndpoint();
+}
+
+/**
+ * Creates a new JSON-RPC listener on the specified port.
+ *
+ * @param node The host the listener should be bound to.
+ * @param service The port to listen on.
+ */
+bool ApiListener::AddListener(const String& node, const String& service)
+{
+ namespace asio = boost::asio;
+ namespace ip = asio::ip;
+ using ip::tcp;
+
+ ObjectLock olock(this);
+
+ if (!m_SSLContext) {
+ Log(LogCritical, "ApiListener", "SSL context is required for AddListener()");
+ return false;
+ }
+
+ auto& io (IoEngine::Get().GetIoContext());
+ auto acceptor (Shared<tcp::acceptor>::Make(io));
+
+ try {
+ 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));
+#ifdef SO_REUSEPORT
+ setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast<const char *>(&optTrue), sizeof(optTrue));
+#endif /* SO_REUSEPORT */
+ }
+
+ acceptor->bind(current->endpoint());
+
+ break;
+ } catch (const std::exception&) {
+ if (++current == result.end()) {
+ throw;
+ }
+
+ if (acceptor->is_open()) {
+ acceptor->close();
+ }
+ }
+ }
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot bind TCP socket for host '" << node << "' on port '" << service << "': " << ex.what();
+ return false;
+ }
+
+ acceptor->listen(INT_MAX);
+
+ auto localEndpoint (acceptor->local_endpoint());
+
+ Log(LogInformation, "ApiListener")
+ << "Started new listener on '[" << localEndpoint.address() << "]:" << localEndpoint.port() << "'";
+
+ IoEngine::SpawnCoroutine(io, [this, acceptor](asio::yield_context yc) { ListenerCoroutineProc(yc, acceptor); });
+
+ UpdateStatusFile(localEndpoint);
+
+ return true;
+}
+
+void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server)
+{
+ namespace asio = boost::asio;
+
+ auto& io (IoEngine::Get().GetIoContext());
+
+ time_t lastModified = -1;
+ const String crlPath = GetCrlPath();
+
+ if (!crlPath.IsEmpty()) {
+ lastModified = Utility::GetFileCreationTime(crlPath);
+ }
+
+ for (;;) {
+ try {
+ asio::ip::tcp::socket socket (io);
+
+ server->async_accept(socket.lowest_layer(), yc);
+
+ if (!crlPath.IsEmpty()) {
+ time_t currentCreationTime = Utility::GetFileCreationTime(crlPath);
+
+ if (lastModified != currentCreationTime) {
+ UpdateSSLContext();
+
+ lastModified = currentCreationTime;
+ }
+ }
+
+ boost::shared_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
+ auto sslConn (Shared<AsioTlsStream>::Make(io, *m_SSLContext));
+
+ lock.unlock();
+ sslConn->lowest_layer() = std::move(socket);
+
+ auto strand (Shared<asio::io_context::strand>::Make(io));
+
+ IoEngine::SpawnCoroutine(*strand, [this, strand, sslConn](asio::yield_context yc) {
+ Timeout::Ptr timeout(new Timeout(strand->context(), *strand, boost::posix_time::microseconds(int64_t(GetConnectTimeout() * 1e6)),
+ [sslConn](asio::yield_context yc) {
+ Log(LogWarning, "ApiListener")
+ << "Timeout while processing incoming connection from "
+ << sslConn->lowest_layer().remote_endpoint();
+
+ boost::system::error_code ec;
+ sslConn->lowest_layer().cancel(ec);
+ }
+ ));
+ Defer cancelTimeout([timeout]() { timeout->Cancel(); });
+
+ NewClientHandler(yc, strand, sslConn, String(), RoleServer);
+ });
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot accept new connection: " << ex.what();
+ }
+ }
+}
+
+/**
+ * Creates a new JSON-RPC client and connects to the specified endpoint.
+ *
+ * @param endpoint The endpoint.
+ */
+void ApiListener::AddConnection(const Endpoint::Ptr& endpoint)
+{
+ namespace asio = boost::asio;
+ using asio::ip::tcp;
+
+ if (!m_SSLContext) {
+ Log(LogCritical, "ApiListener", "SSL context is required for AddConnection()");
+ return;
+ }
+
+ auto& io (IoEngine::Get().GetIoContext());
+ auto strand (Shared<asio::io_context::strand>::Make(io));
+
+ IoEngine::SpawnCoroutine(*strand, [this, strand, endpoint, &io](asio::yield_context yc) {
+ String host = endpoint->GetHost();
+ String port = endpoint->GetPort();
+
+ Log(LogInformation, "ApiListener")
+ << "Reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'";
+
+ try {
+ boost::shared_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
+ auto sslConn (Shared<AsioTlsStream>::Make(io, *m_SSLContext, endpoint->GetName()));
+
+ lock.unlock();
+
+ Timeout::Ptr timeout(new Timeout(strand->context(), *strand, boost::posix_time::microseconds(int64_t(GetConnectTimeout() * 1e6)),
+ [sslConn, endpoint, host, port](asio::yield_context yc) {
+ Log(LogCritical, "ApiListener")
+ << "Timeout while reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host
+ << "' and port '" << port << "', cancelling attempt";
+
+ boost::system::error_code ec;
+ sslConn->lowest_layer().cancel(ec);
+ }
+ ));
+ Defer cancelTimeout([&timeout]() { timeout->Cancel(); });
+
+ Connect(sslConn->lowest_layer(), host, port, yc);
+
+ NewClientHandler(yc, strand, sslConn, endpoint->GetName(), RoleClient);
+
+ endpoint->SetConnecting(false);
+ Log(LogInformation, "ApiListener")
+ << "Finished reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'";
+ } catch (const std::exception& ex) {
+ endpoint->SetConnecting(false);
+
+ Log(LogCritical, "ApiListener")
+ << "Cannot connect to host '" << host << "' on port '" << port << "': " << ex.what();
+ }
+ });
+}
+
+void ApiListener::NewClientHandler(
+ boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
+ const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
+)
+{
+ try {
+ NewClientHandlerInternal(yc, strand, client, hostname, role);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Exception while handling new API client connection: " << DiagnosticInformation(ex, false);
+
+ Log(LogDebug, "ApiListener")
+ << "Exception while handling new API client connection: " << DiagnosticInformation(ex);
+ }
+}
+
+static const auto l_AppVersionInt (([]() -> unsigned long {
+ auto appVersion (Application::GetAppVersion());
+ boost::regex rgx (R"EOF(^[rv]?(\d+)\.(\d+)\.(\d+))EOF");
+ boost::smatch match;
+
+ if (!boost::regex_search(appVersion.GetData(), match, rgx)) {
+ return 0;
+ }
+
+ return 100u * 100u * boost::lexical_cast<unsigned long>(match[1].str())
+ + 100u * boost::lexical_cast<unsigned long>(match[2].str())
+ + boost::lexical_cast<unsigned long>(match[3].str());
+})());
+
+static const auto l_MyCapabilities (
+ (uint_fast64_t)ApiCapabilities::ExecuteArbitraryCommand | (uint_fast64_t)ApiCapabilities::IfwApiCheckCommand
+);
+
+/**
+ * Processes a new client connection.
+ *
+ * @param client The new client.
+ */
+void ApiListener::NewClientHandlerInternal(
+ boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
+ const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
+)
+{
+ namespace asio = boost::asio;
+ namespace ssl = asio::ssl;
+
+ String conninfo;
+
+ {
+ std::ostringstream conninfo_;
+
+ if (role == RoleClient) {
+ conninfo_ << "to";
+ } else {
+ conninfo_ << "from";
+ }
+
+ auto endpoint (client->lowest_layer().remote_endpoint());
+
+ conninfo_ << " [" << endpoint.address() << "]:" << endpoint.port();
+
+ conninfo = conninfo_.str();
+ }
+
+ auto& sslConn (client->next_layer());
+
+ boost::system::error_code ec;
+
+ {
+ Timeout::Ptr handshakeTimeout (new Timeout(
+ strand->context(),
+ *strand,
+ boost::posix_time::microseconds(intmax_t(Configuration::TlsHandshakeTimeout * 1000000)),
+ [strand, client](asio::yield_context yc) {
+ boost::system::error_code ec;
+ client->lowest_layer().cancel(ec);
+ }
+ ));
+
+ sslConn.async_handshake(role == RoleClient ? sslConn.client : sslConn.server, yc[ec]);
+
+ handshakeTimeout->Cancel();
+ }
+
+ if (ec) {
+ // https://github.com/boostorg/beast/issues/915
+ // Google Chrome 73+ seems not close the connection properly, https://stackoverflow.com/questions/56272906/how-to-fix-certificate-unknown-error-from-chrome-v73
+ if (ec == asio::ssl::error::stream_truncated) {
+ Log(LogNotice, "ApiListener")
+ << "TLS stream was truncated, ignoring connection from " << conninfo;
+ return;
+ }
+
+ Log(LogCritical, "ApiListener")
+ << "Client TLS handshake failed (" << conninfo << "): " << ec.message();
+ return;
+ }
+
+ bool willBeShutDown = false;
+
+ Defer shutDownIfNeeded ([&sslConn, &willBeShutDown, &yc]() {
+ if (!willBeShutDown) {
+ // Ignore the error, but do not throw an exception being swallowed at all cost.
+ // https://github.com/Icinga/icinga2/issues/7351
+ boost::system::error_code ec;
+ sslConn.async_shutdown(yc[ec]);
+ }
+ });
+
+ std::shared_ptr<X509> cert (sslConn.GetPeerCertificate());
+ bool verify_ok = false;
+ String identity;
+ Endpoint::Ptr endpoint;
+
+ if (cert) {
+ verify_ok = sslConn.IsVerifyOK();
+
+ String verifyError = sslConn.GetVerifyError();
+
+ try {
+ identity = GetCertificateCN(cert);
+ } catch (const std::exception&) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot get certificate common name from peer (" << conninfo << ") cert.";
+ return;
+ }
+
+ if (!hostname.IsEmpty()) {
+ if (identity != hostname) {
+ Log(LogWarning, "ApiListener")
+ << "Unexpected certificate common name while connecting to endpoint '"
+ << hostname << "': got '" << identity << "'";
+ return;
+ } else if (!verify_ok) {
+ Log(LogWarning, "ApiListener")
+ << "Certificate validation failed for endpoint '" << hostname
+ << "': " << verifyError;
+ }
+ }
+
+ if (verify_ok) {
+ endpoint = Endpoint::GetByName(identity);
+ }
+
+ Log log(LogInformation, "ApiListener");
+
+ log << "New client connection for identity '" << identity << "' " << conninfo;
+
+ if (!verify_ok) {
+ log << " (certificate validation failed: " << verifyError << ")";
+ } else if (!endpoint) {
+ log << " (no Endpoint object found for identity)";
+ }
+ } else {
+ Log(LogInformation, "ApiListener")
+ << "New client connection " << conninfo << " (no client certificate)";
+ }
+
+ ClientType ctype;
+
+ try {
+ if (role == RoleClient) {
+ JsonRpc::SendMessage(client, new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "icinga::Hello" },
+ { "params", new Dictionary({
+ { "version", (double)l_AppVersionInt },
+ { "capabilities", (double)l_MyCapabilities }
+ }) }
+ }), yc);
+
+ client->async_flush(yc);
+
+ ctype = ClientJsonRpc;
+ } else {
+ {
+ boost::system::error_code ec;
+
+ if (client->async_fill(yc[ec]) == 0u) {
+ if (identity.IsEmpty()) {
+ Log(LogInformation, "ApiListener")
+ << "No data received on new API connection " << conninfo << ". "
+ << "Ensure that the remote endpoints are properly configured in a cluster setup.";
+ } else {
+ Log(LogWarning, "ApiListener")
+ << "No data received on new API connection " << conninfo << " for identity '" << identity << "'. "
+ << "Ensure that the remote endpoints are properly configured in a cluster setup.";
+ }
+
+ return;
+ }
+ }
+
+ char firstByte = 0;
+
+ {
+ asio::mutable_buffer firstByteBuf (&firstByte, 1);
+ client->peek(firstByteBuf);
+ }
+
+ if (firstByte >= '0' && firstByte <= '9') {
+ JsonRpc::SendMessage(client, new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "icinga::Hello" },
+ { "params", new Dictionary({
+ { "version", (double)l_AppVersionInt },
+ { "capabilities", (double)l_MyCapabilities }
+ }) }
+ }), yc);
+
+ client->async_flush(yc);
+
+ ctype = ClientJsonRpc;
+ } else {
+ ctype = ClientHttp;
+ }
+ }
+ } catch (const boost::system::system_error& systemError) {
+ if (systemError.code() == boost::asio::error::operation_aborted) {
+ shutDownIfNeeded.Cancel();
+ }
+
+ throw;
+ }
+
+ if (ctype == ClientJsonRpc) {
+ Log(LogNotice, "ApiListener", "New JSON-RPC client");
+
+ if (endpoint && endpoint->GetConnected()) {
+ Log(LogNotice, "ApiListener")
+ << "Ignoring JSON-RPC connection " << conninfo
+ << ". We're already connected to Endpoint '" << endpoint->GetName() << "'.";
+ return;
+ }
+
+ JsonRpcConnection::Ptr aclient = new JsonRpcConnection(identity, verify_ok, client, role);
+
+ if (endpoint) {
+ endpoint->AddClient(aclient);
+
+ Utility::QueueAsyncCallback([this, aclient, endpoint]() {
+ SyncClient(aclient, endpoint, true);
+ });
+ } else if (!AddAnonymousClient(aclient)) {
+ Log(LogNotice, "ApiListener")
+ << "Ignoring anonymous JSON-RPC connection " << conninfo
+ << ". Max connections (" << GetMaxAnonymousClients() << ") exceeded.";
+
+ aclient = nullptr;
+ }
+
+ if (aclient) {
+ aclient->Start();
+
+ willBeShutDown = true;
+ }
+ } else {
+ Log(LogNotice, "ApiListener", "New HTTP client");
+
+ HttpServerConnection::Ptr aclient = new HttpServerConnection(identity, verify_ok, client);
+ AddHttpClient(aclient);
+ aclient->Start();
+
+ willBeShutDown = true;
+ }
+}
+
+void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoint::Ptr& endpoint, bool needSync)
+{
+ Zone::Ptr eZone = endpoint->GetZone();
+
+ try {
+ {
+ ObjectLock olock(endpoint);
+
+ endpoint->SetSyncing(true);
+ }
+
+ Zone::Ptr myZone = Zone::GetLocalZone();
+ auto parent (myZone->GetParent());
+
+ if (parent == eZone || (!parent && eZone == myZone)) {
+ JsonRpcConnection::SendCertificateRequest(aclient, nullptr, String());
+
+ if (Utility::PathExists(ApiListener::GetCertificateRequestsDir())) {
+ Utility::Glob(ApiListener::GetCertificateRequestsDir() + "/*.json", [aclient](const String& newPath) {
+ JsonRpcConnection::SendCertificateRequest(aclient, nullptr, newPath);
+ }, GlobFile);
+ }
+ }
+
+ /* Make sure that the config updates are synced
+ * before the logs are replayed.
+ */
+
+ Log(LogInformation, "ApiListener")
+ << "Sending config updates for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+
+ /* sync zone file config */
+ SendConfigUpdate(aclient);
+
+ Log(LogInformation, "ApiListener")
+ << "Finished sending config file updates for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+
+ /* sync runtime config */
+ SendRuntimeConfigObjects(aclient);
+
+ Log(LogInformation, "ApiListener")
+ << "Finished sending runtime config updates for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+
+ if (!needSync) {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ return;
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Sending replay log for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+
+ ReplayLog(aclient);
+
+ if (eZone == Zone::GetLocalZone())
+ UpdateObjectAuthority();
+
+ Log(LogInformation, "ApiListener")
+ << "Finished sending replay log for endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+ } catch (const std::exception& ex) {
+ {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ }
+
+ Log(LogCritical, "ApiListener")
+ << "Error while syncing endpoint '" << endpoint->GetName() << "': " << DiagnosticInformation(ex, false);
+
+ Log(LogDebug, "ApiListener")
+ << "Error while syncing endpoint '" << endpoint->GetName() << "': " << DiagnosticInformation(ex);
+ }
+
+ Log(LogInformation, "ApiListener")
+ << "Finished syncing endpoint '" << endpoint->GetName() << "' in zone '" << eZone->GetName() << "'.";
+}
+
+void ApiListener::ApiTimerHandler()
+{
+ double now = Utility::GetTime();
+
+ std::vector<int> files;
+ Utility::Glob(GetApiDir() + "log/*", [&files](const String& file) { LogGlobHandler(files, file); }, GlobFile);
+ std::sort(files.begin(), files.end());
+
+ for (int ts : files) {
+ bool need = false;
+ auto localZone (GetLocalEndpoint()->GetZone());
+
+ for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
+ if (endpoint == GetLocalEndpoint())
+ continue;
+
+ auto zone (endpoint->GetZone());
+
+ /* only care for endpoints in a) the same zone b) our parent zone c) immediate child zones */
+ if (!(zone == localZone || zone == localZone->GetParent() || zone->GetParent() == localZone)) {
+ continue;
+ }
+
+ if (endpoint->GetLogDuration() >= 0 && ts < now - endpoint->GetLogDuration())
+ continue;
+
+ if (ts > endpoint->GetLocalLogPosition()) {
+ need = true;
+ break;
+ }
+ }
+
+ if (!need) {
+ String path = GetApiDir() + "log/" + Convert::ToString(ts);
+ Log(LogNotice, "ApiListener")
+ << "Removing old log file: " << path;
+ (void)unlink(path.CStr());
+ }
+ }
+
+ for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
+ if (!endpoint->GetConnected())
+ continue;
+
+ double ts = endpoint->GetRemoteLogPosition();
+
+ if (ts == 0)
+ continue;
+
+ Dictionary::Ptr lmessage = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "log::SetLogPosition" },
+ { "params", new Dictionary({
+ { "log_position", ts }
+ }) }
+ });
+
+ double maxTs = 0;
+
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ if (client->GetTimestamp() > maxTs)
+ maxTs = client->GetTimestamp();
+ }
+
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ if (client->GetTimestamp() == maxTs) {
+ client->SendMessage(lmessage);
+ } else {
+ client->Disconnect();
+ }
+ }
+
+ Log(LogNotice, "ApiListener")
+ << "Setting log position for identity '" << endpoint->GetName() << "': "
+ << Utility::FormatDateTime("%Y/%m/%d %H:%M:%S", ts);
+ }
+}
+
+void ApiListener::ApiReconnectTimerHandler()
+{
+ Zone::Ptr my_zone = Zone::GetLocalZone();
+
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ /* don't connect to global zones */
+ if (zone->GetGlobal())
+ continue;
+
+ /* only connect to endpoints in a) the same zone b) our parent zone c) immediate child zones */
+ if (my_zone != zone && my_zone != zone->GetParent() && zone != my_zone->GetParent()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Zone '" << zone->GetName()
+ << "' because it's not in the same zone, a parent or a child zone.";
+ continue;
+ }
+
+ for (const Endpoint::Ptr& endpoint : zone->GetEndpoints()) {
+ /* don't connect to ourselves */
+ if (endpoint == GetLocalEndpoint()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Endpoint '" << endpoint->GetName() << "' because that's us.";
+ continue;
+ }
+
+ /* don't try to connect to endpoints which don't have a host and port */
+ if (endpoint->GetHost().IsEmpty() || endpoint->GetPort().IsEmpty()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Endpoint '" << endpoint->GetName()
+ << "' because the host/port attributes are missing.";
+ continue;
+ }
+
+ /* don't try to connect if there's already a connection attempt */
+ if (endpoint->GetConnecting()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Endpoint '" << endpoint->GetName()
+ << "' because we're already trying to connect to it.";
+ continue;
+ }
+
+ /* don't try to connect if we're already connected */
+ if (endpoint->GetConnected()) {
+ Log(LogDebug, "ApiListener")
+ << "Not connecting to Endpoint '" << endpoint->GetName()
+ << "' because we're already connected to it.";
+ continue;
+ }
+
+ /* Set connecting state to prevent duplicated queue inserts later. */
+ endpoint->SetConnecting(true);
+
+ AddConnection(endpoint);
+ }
+ }
+
+ Endpoint::Ptr master = GetMaster();
+
+ if (master)
+ Log(LogNotice, "ApiListener")
+ << "Current zone master: " << master->GetName();
+
+ std::vector<String> names;
+ for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>())
+ if (endpoint->GetConnected())
+ names.emplace_back(endpoint->GetName() + " (" + Convert::ToString(endpoint->GetClients().size()) + ")");
+
+ Log(LogNotice, "ApiListener")
+ << "Connected endpoints: " << Utility::NaturalJoin(names);
+}
+
+static void CleanupCertificateRequest(const String& path, double expiryTime)
+{
+#ifndef _WIN32
+ struct stat statbuf;
+ if (lstat(path.CStr(), &statbuf) < 0)
+ return;
+#else /* _WIN32 */
+ struct _stat statbuf;
+ if (_stat(path.CStr(), &statbuf) < 0)
+ return;
+#endif /* _WIN32 */
+
+ if (statbuf.st_mtime < expiryTime)
+ (void) unlink(path.CStr());
+}
+
+void ApiListener::CleanupCertificateRequestsTimerHandler()
+{
+ String requestsDir = GetCertificateRequestsDir();
+
+ if (Utility::PathExists(requestsDir)) {
+ /* remove certificate requests that are older than a week */
+ double expiryTime = Utility::GetTime() - 7 * 24 * 60 * 60;
+ Utility::Glob(requestsDir + "/*.json", [expiryTime](const String& path) {
+ CleanupCertificateRequest(path, expiryTime);
+ }, GlobFile);
+ }
+}
+
+void ApiListener::RelayMessage(const MessageOrigin::Ptr& origin,
+ const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log)
+{
+ if (!IsActive())
+ return;
+
+ m_RelayQueue.Enqueue([this, origin, secobj, message, log]() { SyncRelayMessage(origin, secobj, message, log); }, PriorityNormal, true);
+}
+
+void ApiListener::PersistMessage(const Dictionary::Ptr& message, const ConfigObject::Ptr& secobj)
+{
+ double ts = message->Get("ts");
+
+ ASSERT(ts != 0);
+
+ Dictionary::Ptr pmessage = new Dictionary();
+ pmessage->Set("timestamp", ts);
+
+ pmessage->Set("message", JsonEncode(message));
+
+ if (secobj) {
+ Dictionary::Ptr secname = new Dictionary();
+ secname->Set("type", secobj->GetReflectionType()->GetName());
+ secname->Set("name", secobj->GetName());
+ pmessage->Set("secobj", secname);
+ }
+
+ std::unique_lock<std::mutex> lock(m_LogLock);
+ if (m_LogFile) {
+ NetString::WriteStringToStream(m_LogFile, JsonEncode(pmessage));
+ m_LogMessageCount++;
+ SetLogMessageTimestamp(ts);
+
+ if (m_LogMessageCount > 50000) {
+ CloseLogFile();
+ RotateLogFile();
+ OpenLogFile();
+ }
+ }
+}
+
+void ApiListener::SyncSendMessage(const Endpoint::Ptr& endpoint, const Dictionary::Ptr& message)
+{
+ ObjectLock olock(endpoint);
+
+ if (!endpoint->GetSyncing()) {
+ Log(LogNotice, "ApiListener")
+ << "Sending message '" << message->Get("method") << "' to '" << endpoint->GetName() << "'";
+
+ double maxTs = 0;
+
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ if (client->GetTimestamp() > maxTs)
+ maxTs = client->GetTimestamp();
+ }
+
+ for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
+ if (client->GetTimestamp() != maxTs)
+ continue;
+
+ client->SendMessage(message);
+ }
+ }
+}
+
+/**
+ * Relay a message to a directly connected zone or to a global zone.
+ * If some other zone is passed as the target zone, it is not relayed.
+ *
+ * @param targetZone The zone to relay to
+ * @param origin Information about where this message is relayed from (if it was not generated locally)
+ * @param message The message to relay
+ * @param currentZoneMaster The current master node of the local zone
+ * @return true if the message has been relayed to all relevant endpoints,
+ * false if it hasn't and must be persisted in the replay log
+ */
+bool ApiListener::RelayMessageOne(const Zone::Ptr& targetZone, const MessageOrigin::Ptr& origin, const Dictionary::Ptr& message, const Endpoint::Ptr& currentZoneMaster)
+{
+ ASSERT(targetZone);
+
+ Zone::Ptr localZone = Zone::GetLocalZone();
+
+ /* only relay the message to a) the same local zone, b) the parent zone and c) direct child zones. Exception is a global zone. */
+ if (!targetZone->GetGlobal() &&
+ targetZone != localZone &&
+ targetZone != localZone->GetParent() &&
+ targetZone->GetParent() != localZone) {
+ return true;
+ }
+
+ Endpoint::Ptr localEndpoint = GetLocalEndpoint();
+
+ std::vector<Endpoint::Ptr> skippedEndpoints;
+
+ std::set<Zone::Ptr> allTargetZones;
+ if (targetZone->GetGlobal()) {
+ /* if the zone is global, the message has to be relayed to our local zone and direct children */
+ allTargetZones.insert(localZone);
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ if (zone->GetParent() == localZone) {
+ allTargetZones.insert(zone);
+ }
+ }
+ } else {
+ /* whereas if it's not global, the message is just relayed to the zone itself */
+ allTargetZones.insert(targetZone);
+ }
+
+ bool needsReplay = false;
+
+ for (const Zone::Ptr& currentTargetZone : allTargetZones) {
+ bool relayed = false, log_needed = false, log_done = false;
+
+ for (const Endpoint::Ptr& targetEndpoint : currentTargetZone->GetEndpoints()) {
+ /* Don't relay messages to ourselves. */
+ if (targetEndpoint == localEndpoint)
+ continue;
+
+ log_needed = true;
+
+ /* Don't relay messages to disconnected endpoints. */
+ if (!targetEndpoint->GetConnected()) {
+ if (currentTargetZone == localZone)
+ log_done = false;
+
+ continue;
+ }
+
+ log_done = true;
+
+ /* Don't relay the message to the zone through more than one endpoint unless this is our own zone.
+ * 'relayed' is set to true on success below, enabling the checks in the second iteration.
+ */
+ if (relayed && currentTargetZone != localZone) {
+ skippedEndpoints.push_back(targetEndpoint);
+ continue;
+ }
+
+ /* Don't relay messages back to the endpoint which we got the message from. */
+ if (origin && origin->FromClient && targetEndpoint == origin->FromClient->GetEndpoint()) {
+ skippedEndpoints.push_back(targetEndpoint);
+ continue;
+ }
+
+ /* Don't relay messages back to the zone which we got the message from. */
+ if (origin && origin->FromZone && currentTargetZone == origin->FromZone) {
+ skippedEndpoints.push_back(targetEndpoint);
+ continue;
+ }
+
+ /* Only relay message to the zone master if we're not currently the zone master.
+ * e1 is zone master, e2 and e3 are zone members.
+ *
+ * Message is sent from e2 or e3:
+ * !isMaster == true
+ * targetEndpoint e1 is zone master -> send the message
+ * targetEndpoint e3 is not zone master -> skip it, avoid routing loops
+ *
+ * Message is sent from e1:
+ * !isMaster == false -> send the messages to e2 and e3 being the zone routing master.
+ */
+ bool isMaster = (currentZoneMaster == localEndpoint);
+
+ if (!isMaster && targetEndpoint != currentZoneMaster) {
+ skippedEndpoints.push_back(targetEndpoint);
+ continue;
+ }
+
+ relayed = true;
+
+ SyncSendMessage(targetEndpoint, message);
+ }
+
+ if (log_needed && !log_done) {
+ needsReplay = true;
+ }
+ }
+
+ if (!skippedEndpoints.empty()) {
+ double ts = message->Get("ts");
+
+ for (const Endpoint::Ptr& skippedEndpoint : skippedEndpoints)
+ skippedEndpoint->SetLocalLogPosition(ts);
+ }
+
+ return !needsReplay;
+}
+
+void ApiListener::SyncRelayMessage(const MessageOrigin::Ptr& origin,
+ const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log)
+{
+ double ts = Utility::GetTime();
+ message->Set("ts", ts);
+
+ Log(LogNotice, "ApiListener")
+ << "Relaying '" << message->Get("method") << "' message";
+
+ if (origin && origin->FromZone)
+ message->Set("originZone", origin->FromZone->GetName());
+
+ Zone::Ptr target_zone;
+
+ if (secobj) {
+ if (secobj->GetReflectionType() == Zone::TypeInstance)
+ target_zone = static_pointer_cast<Zone>(secobj);
+ else
+ target_zone = static_pointer_cast<Zone>(secobj->GetZone());
+ }
+
+ if (!target_zone)
+ target_zone = Zone::GetLocalZone();
+
+ Endpoint::Ptr master = GetMaster();
+
+ bool need_log = !RelayMessageOne(target_zone, origin, message, master);
+
+ for (const Zone::Ptr& zone : target_zone->GetAllParentsRaw()) {
+ if (!RelayMessageOne(zone, origin, message, master))
+ need_log = true;
+ }
+
+ if (log && need_log)
+ PersistMessage(message, secobj);
+}
+
+/* must hold m_LogLock */
+void ApiListener::OpenLogFile()
+{
+ String path = GetApiDir() + "log/current";
+
+ Utility::MkDirP(Utility::DirName(path), 0750);
+
+ std::unique_ptr<std::fstream> fp = std::make_unique<std::fstream>(path.CStr(), std::fstream::out | std::ofstream::app);
+
+ if (!fp->good()) {
+ Log(LogWarning, "ApiListener")
+ << "Could not open spool file: " << path;
+ return;
+ }
+
+ m_LogFile = new StdioStream(fp.release(), true);
+ m_LogMessageCount = 0;
+ SetLogMessageTimestamp(Utility::GetTime());
+}
+
+/* must hold m_LogLock */
+void ApiListener::CloseLogFile()
+{
+ if (!m_LogFile)
+ return;
+
+ m_LogFile->Close();
+ m_LogFile.reset();
+}
+
+/* must hold m_LogLock */
+void ApiListener::RotateLogFile()
+{
+ double ts = GetLogMessageTimestamp();
+
+ if (ts == 0)
+ ts = Utility::GetTime();
+
+ String oldpath = GetApiDir() + "log/current";
+ String newpath = GetApiDir() + "log/" + Convert::ToString(static_cast<int>(ts)+1);
+
+ // If the log is being rotated more than once per second,
+ // don't overwrite the previous one, but silently deny rotation.
+ if (!Utility::PathExists(newpath)) {
+ try {
+ Utility::RenameFile(oldpath, newpath);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << "Cannot rotate replay log file from '" << oldpath << "' to '"
+ << newpath << "': " << ex.what();
+ }
+ }
+}
+
+void ApiListener::LogGlobHandler(std::vector<int>& files, const String& file)
+{
+ String name = Utility::BaseName(file);
+
+ if (name == "current")
+ return;
+
+ int ts;
+
+ try {
+ ts = Convert::ToLong(name);
+ } catch (const std::exception&) {
+ return;
+ }
+
+ files.push_back(ts);
+}
+
+void ApiListener::ReplayLog(const JsonRpcConnection::Ptr& client)
+{
+ Endpoint::Ptr endpoint = client->GetEndpoint();
+
+ if (endpoint->GetLogDuration() == 0) {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ return;
+ }
+
+ CONTEXT("Replaying log for Endpoint '" << endpoint->GetName() << "'");
+
+ int count = -1;
+ double peer_ts = endpoint->GetLocalLogPosition();
+ double logpos_ts = peer_ts;
+ bool last_sync = false;
+
+ Endpoint::Ptr target_endpoint = client->GetEndpoint();
+ ASSERT(target_endpoint);
+
+ Zone::Ptr target_zone = target_endpoint->GetZone();
+
+ if (!target_zone) {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ return;
+ }
+
+ for (;;) {
+ std::unique_lock<std::mutex> lock(m_LogLock);
+
+ CloseLogFile();
+
+ if (count == -1 || count > 50000) {
+ OpenLogFile();
+ lock.unlock();
+ } else {
+ last_sync = true;
+ }
+
+ count = 0;
+
+ std::vector<int> files;
+ Utility::Glob(GetApiDir() + "log/*", [&files](const String& file) { LogGlobHandler(files, file); }, GlobFile);
+ std::sort(files.begin(), files.end());
+
+ std::vector<std::pair<int, String>> allFiles;
+
+ for (int ts : files) {
+ if (ts >= peer_ts) {
+ allFiles.emplace_back(ts, GetApiDir() + "log/" + Convert::ToString(ts));
+ }
+ }
+
+ allFiles.emplace_back(Utility::GetTime() + 1, GetApiDir() + "log/current");
+
+ for (auto& file : allFiles) {
+ Log(LogNotice, "ApiListener")
+ << "Replaying log: " << file.second;
+
+ auto *fp = new std::fstream(file.second.CStr(), std::fstream::in | std::fstream::binary);
+ StdioStream::Ptr logStream = new StdioStream(fp, true);
+
+ String message;
+ StreamReadContext src;
+ while (true) {
+ Dictionary::Ptr pmessage;
+
+ try {
+ StreamReadStatus srs = NetString::ReadStringFromStream(logStream, &message, src);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ pmessage = JsonDecode(message);
+ } catch (const std::exception&) {
+ Log(LogWarning, "ApiListener")
+ << "Unexpected end-of-file for cluster log: " << file.second;
+
+ /* Log files may be incomplete or corrupted. This is perfectly OK. */
+ break;
+ }
+
+ if (pmessage->Get("timestamp") <= peer_ts)
+ continue;
+
+ Dictionary::Ptr secname = pmessage->Get("secobj");
+
+ if (secname) {
+ ConfigObject::Ptr secobj = ConfigObject::GetObject(secname->Get("type"), secname->Get("name"));
+
+ if (!secobj)
+ continue;
+
+ if (!target_zone->CanAccessObject(secobj))
+ continue;
+ }
+
+ try {
+ client->SendRawMessage(pmessage->Get("message"));
+ count++;
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "ApiListener")
+ << "Error while replaying log for endpoint '" << endpoint->GetName() << "': " << DiagnosticInformation(ex, false);
+
+ Log(LogDebug, "ApiListener")
+ << "Error while replaying log for endpoint '" << endpoint->GetName() << "': " << DiagnosticInformation(ex);
+
+ break;
+ }
+
+ peer_ts = pmessage->Get("timestamp");
+
+ if (file.first > logpos_ts + 10) {
+ logpos_ts = file.first;
+
+ Dictionary::Ptr lmessage = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "log::SetLogPosition" },
+ { "params", new Dictionary({
+ { "log_position", logpos_ts }
+ }) }
+ });
+
+ client->SendMessage(lmessage);
+ }
+ }
+
+ logStream->Close();
+ }
+
+ if (count > 0) {
+ Log(LogInformation, "ApiListener")
+ << "Replayed " << count << " messages.";
+ }
+ else {
+ Log(LogNotice, "ApiListener")
+ << "Replayed " << count << " messages.";
+ }
+
+ if (last_sync) {
+ {
+ ObjectLock olock2(endpoint);
+ endpoint->SetSyncing(false);
+ }
+
+ OpenLogFile();
+
+ break;
+ }
+ }
+}
+
+void ApiListener::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata)
+{
+ std::pair<Dictionary::Ptr, Dictionary::Ptr> stats;
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ stats = listener->GetStatus();
+
+ ObjectLock olock(stats.second);
+ for (const Dictionary::Pair& kv : stats.second)
+ perfdata->Add(new PerfdataValue("api_" + kv.first, kv.second));
+
+ status->Set("api", stats.first);
+}
+
+std::pair<Dictionary::Ptr, Dictionary::Ptr> ApiListener::GetStatus()
+{
+ Dictionary::Ptr perfdata = new Dictionary();
+
+ /* cluster stats */
+
+ double allEndpoints = 0;
+ Array::Ptr allNotConnectedEndpoints = new Array();
+ Array::Ptr allConnectedEndpoints = new Array();
+
+ Zone::Ptr my_zone = Zone::GetLocalZone();
+
+ Dictionary::Ptr connectedZones = new Dictionary();
+
+ for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
+ /* only check endpoints in a) the same zone b) our parent zone c) immediate child zones */
+ if (my_zone != zone && my_zone != zone->GetParent() && zone != my_zone->GetParent()) {
+ Log(LogDebug, "ApiListener")
+ << "Not checking connection to Zone '" << zone->GetName() << "' because it's not in the same zone, a parent or a child zone.";
+ continue;
+ }
+
+ bool zoneConnected = false;
+ int countZoneEndpoints = 0;
+ double zoneLag = 0;
+
+ ArrayData zoneEndpoints;
+
+ for (const Endpoint::Ptr& endpoint : zone->GetEndpoints()) {
+ zoneEndpoints.emplace_back(endpoint->GetName());
+
+ if (endpoint->GetName() == GetIdentity())
+ continue;
+
+ double eplag = CalculateZoneLag(endpoint);
+
+ if (eplag > 0 && eplag > zoneLag)
+ zoneLag = eplag;
+
+ allEndpoints++;
+ countZoneEndpoints++;
+
+ if (!endpoint->GetConnected()) {
+ allNotConnectedEndpoints->Add(endpoint->GetName());
+ } else {
+ allConnectedEndpoints->Add(endpoint->GetName());
+ zoneConnected = true;
+ }
+ }
+
+ /* if there's only one endpoint inside the zone, we're not connected - that's us, fake it */
+ if (zone->GetEndpoints().size() == 1 && countZoneEndpoints == 0)
+ zoneConnected = true;
+
+ String parentZoneName;
+ Zone::Ptr parentZone = zone->GetParent();
+ if (parentZone)
+ parentZoneName = parentZone->GetName();
+
+ Dictionary::Ptr zoneStats = new Dictionary({
+ { "connected", zoneConnected },
+ { "client_log_lag", zoneLag },
+ { "endpoints", new Array(std::move(zoneEndpoints)) },
+ { "parent_zone", parentZoneName }
+ });
+
+ connectedZones->Set(zone->GetName(), zoneStats);
+ }
+
+ /* connection stats */
+ size_t jsonRpcAnonymousClients = GetAnonymousClients().size();
+ size_t httpClients = GetHttpClients().size();
+ size_t syncQueueItems = m_SyncQueue.GetLength();
+ size_t relayQueueItems = m_RelayQueue.GetLength();
+ double workQueueItemRate = JsonRpcConnection::GetWorkQueueRate();
+ double syncQueueItemRate = m_SyncQueue.GetTaskCount(60) / 60.0;
+ double relayQueueItemRate = m_RelayQueue.GetTaskCount(60) / 60.0;
+
+ Dictionary::Ptr status = new Dictionary({
+ { "identity", GetIdentity() },
+ { "num_endpoints", allEndpoints },
+ { "num_conn_endpoints", allConnectedEndpoints->GetLength() },
+ { "num_not_conn_endpoints", allNotConnectedEndpoints->GetLength() },
+ { "conn_endpoints", allConnectedEndpoints },
+ { "not_conn_endpoints", allNotConnectedEndpoints },
+
+ { "zones", connectedZones },
+
+ { "json_rpc", new Dictionary({
+ { "anonymous_clients", jsonRpcAnonymousClients },
+ { "sync_queue_items", syncQueueItems },
+ { "relay_queue_items", relayQueueItems },
+ { "work_queue_item_rate", workQueueItemRate },
+ { "sync_queue_item_rate", syncQueueItemRate },
+ { "relay_queue_item_rate", relayQueueItemRate }
+ }) },
+
+ { "http", new Dictionary({
+ { "clients", httpClients }
+ }) }
+ });
+
+ /* performance data */
+ perfdata->Set("num_endpoints", allEndpoints);
+ perfdata->Set("num_conn_endpoints", Convert::ToDouble(allConnectedEndpoints->GetLength()));
+ perfdata->Set("num_not_conn_endpoints", Convert::ToDouble(allNotConnectedEndpoints->GetLength()));
+
+ perfdata->Set("num_json_rpc_anonymous_clients", jsonRpcAnonymousClients);
+ perfdata->Set("num_http_clients", httpClients);
+ perfdata->Set("num_json_rpc_sync_queue_items", syncQueueItems);
+ perfdata->Set("num_json_rpc_relay_queue_items", relayQueueItems);
+
+ perfdata->Set("num_json_rpc_work_queue_item_rate", workQueueItemRate);
+ perfdata->Set("num_json_rpc_sync_queue_item_rate", syncQueueItemRate);
+ perfdata->Set("num_json_rpc_relay_queue_item_rate", relayQueueItemRate);
+
+ return std::make_pair(status, perfdata);
+}
+
+double ApiListener::CalculateZoneLag(const Endpoint::Ptr& endpoint)
+{
+ double remoteLogPosition = endpoint->GetRemoteLogPosition();
+ double eplag = Utility::GetTime() - remoteLogPosition;
+
+ if ((endpoint->GetSyncing() || !endpoint->GetConnected()) && remoteLogPosition != 0)
+ return eplag;
+
+ return 0;
+}
+
+bool ApiListener::AddAnonymousClient(const JsonRpcConnection::Ptr& aclient)
+{
+ std::unique_lock<std::mutex> lock(m_AnonymousClientsLock);
+
+ if (GetMaxAnonymousClients() >= 0 && (long)m_AnonymousClients.size() + 1 > (long)GetMaxAnonymousClients())
+ return false;
+
+ m_AnonymousClients.insert(aclient);
+ return true;
+}
+
+void ApiListener::RemoveAnonymousClient(const JsonRpcConnection::Ptr& aclient)
+{
+ std::unique_lock<std::mutex> lock(m_AnonymousClientsLock);
+ m_AnonymousClients.erase(aclient);
+}
+
+std::set<JsonRpcConnection::Ptr> ApiListener::GetAnonymousClients() const
+{
+ std::unique_lock<std::mutex> lock(m_AnonymousClientsLock);
+ return m_AnonymousClients;
+}
+
+void ApiListener::AddHttpClient(const HttpServerConnection::Ptr& aclient)
+{
+ std::unique_lock<std::mutex> lock(m_HttpClientsLock);
+ m_HttpClients.insert(aclient);
+}
+
+void ApiListener::RemoveHttpClient(const HttpServerConnection::Ptr& aclient)
+{
+ std::unique_lock<std::mutex> lock(m_HttpClientsLock);
+ m_HttpClients.erase(aclient);
+}
+
+std::set<HttpServerConnection::Ptr> ApiListener::GetHttpClients() const
+{
+ std::unique_lock<std::mutex> lock(m_HttpClientsLock);
+ return m_HttpClients;
+}
+
+static void LogAppVersion(unsigned long version, Log& log)
+{
+ log << version / 100u << "." << version % 100u << ".x";
+}
+
+Value ApiListener::HelloAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ if (origin) {
+ auto client (origin->FromClient);
+
+ if (client) {
+ auto endpoint (client->GetEndpoint());
+
+ if (endpoint) {
+ unsigned long nodeVersion = params->Get("version");
+
+ endpoint->SetIcingaVersion(nodeVersion);
+ endpoint->SetCapabilities((double)params->Get("capabilities"));
+
+ if (nodeVersion == 0u) {
+ nodeVersion = 21200;
+ }
+
+ if (endpoint->GetZone()->GetParent() == Zone::GetLocalZone()) {
+ switch (l_AppVersionInt / 100 - nodeVersion / 100) {
+ case 0:
+ case 1:
+ break;
+ default:
+ Log log (LogWarning, "ApiListener");
+ log << "Unexpected Icinga version of endpoint '" << endpoint->GetName() << "': ";
+
+ LogAppVersion(nodeVersion / 100u, log);
+ log << " Expected one of: ";
+
+ LogAppVersion(l_AppVersionInt / 100u, log);
+ log << ", ";
+
+ LogAppVersion((l_AppVersionInt / 100u - 1u), log);
+ }
+ }
+ }
+ }
+ }
+
+ return Empty;
+}
+
+Endpoint::Ptr ApiListener::GetLocalEndpoint() const
+{
+ return m_LocalEndpoint;
+}
+
+void ApiListener::UpdateActivePackageStagesCache()
+{
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+
+ for (auto package : ConfigPackageUtility::GetPackages()) {
+ String activeStage;
+
+ try {
+ activeStage = ConfigPackageUtility::GetActiveStageFromFile(package);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ApiListener")
+ << ex.what();
+ continue;
+ }
+
+ Log(LogNotice, "ApiListener")
+ << "Updating cache: Config package '" << package << "' has active stage '" << activeStage << "'.";
+
+ m_ActivePackageStages[package] = activeStage;
+ }
+}
+
+void ApiListener::CheckApiPackageIntegrity()
+{
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+
+ for (auto package : ConfigPackageUtility::GetPackages()) {
+ String activeStage;
+ try {
+ activeStage = ConfigPackageUtility::GetActiveStageFromFile(package);
+ } catch (const std::exception& ex) {
+ /* An error means that the stage is broken, try to repair it. */
+ auto it = m_ActivePackageStages.find(package);
+
+ if (it == m_ActivePackageStages.end())
+ continue;
+
+ String activeStageCached = it->second;
+
+ Log(LogInformation, "ApiListener")
+ << "Repairing broken API config package '" << package
+ << "', setting active stage '" << activeStageCached << "'.";
+
+ ConfigPackageUtility::SetActiveStageToFile(package, activeStageCached);
+ }
+ }
+}
+
+void ApiListener::SetActivePackageStage(const String& package, const String& stage)
+{
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+ m_ActivePackageStages[package] = stage;
+}
+
+String ApiListener::GetActivePackageStage(const String& package)
+{
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+
+ if (m_ActivePackageStages.find(package) == m_ActivePackageStages.end())
+ BOOST_THROW_EXCEPTION(ScriptError("Package " + package + " has no active stage."));
+
+ return m_ActivePackageStages[package];
+}
+
+void ApiListener::RemoveActivePackageStage(const String& package)
+{
+ /* This is the rare occassion when a package has been deleted. */
+ std::unique_lock<std::mutex> lock(m_ActivePackageStagesLock);
+
+ auto it = m_ActivePackageStages.find(package);
+
+ if (it == m_ActivePackageStages.end())
+ return;
+
+ m_ActivePackageStages.erase(it);
+}
+
+void ApiListener::ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<ApiListener>::ValidateTlsProtocolmin(lvalue, utils);
+
+ try {
+ ResolveTlsProtocolVersion(lvalue());
+ } catch (const std::exception& ex) {
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "tls_protocolmin" }, ex.what()));
+ }
+}
+
+void ApiListener::ValidateTlsHandshakeTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<ApiListener>::ValidateTlsHandshakeTimeout(lvalue, utils);
+
+ if (lvalue() <= 0)
+ BOOST_THROW_EXCEPTION(ValidationError(this, { "tls_handshake_timeout" }, "Value must be greater than 0."));
+}
+
+bool ApiListener::IsHACluster()
+{
+ Zone::Ptr zone = Zone::GetLocalZone();
+
+ if (!zone)
+ return false;
+
+ return zone->IsSingleInstance();
+}
+
+/* Provide a helper function for zone origin name. */
+String ApiListener::GetFromZoneName(const Zone::Ptr& fromZone)
+{
+ String fromZoneName;
+
+ if (fromZone) {
+ fromZoneName = fromZone->GetName();
+ } else {
+ Zone::Ptr lzone = Zone::GetLocalZone();
+
+ if (lzone)
+ fromZoneName = lzone->GetName();
+ }
+
+ return fromZoneName;
+}
+
+void ApiListener::UpdateStatusFile(boost::asio::ip::tcp::endpoint localEndpoint)
+{
+ String path = Configuration::CacheDir + "/api-state.json";
+
+ Utility::SaveJsonFile(path, 0644, new Dictionary({
+ {"host", String(localEndpoint.address().to_string())},
+ {"port", localEndpoint.port()}
+ }));
+}
+
+void ApiListener::RemoveStatusFile()
+{
+ String path = Configuration::CacheDir + "/api-state.json";
+
+ Utility::Remove(path);
+}
diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp
new file mode 100644
index 0000000..fced0a8
--- /dev/null
+++ b/lib/remote/apilistener.hpp
@@ -0,0 +1,265 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APILISTENER_H
+#define APILISTENER_H
+
+#include "remote/apilistener-ti.hpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/httpserverconnection.hpp"
+#include "remote/endpoint.hpp"
+#include "remote/messageorigin.hpp"
+#include "base/configobject.hpp"
+#include "base/process.hpp"
+#include "base/shared.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/tlsstream.hpp"
+#include "base/threadpool.hpp"
+#include <atomic>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/thread/shared_mutex.hpp>
+#include <cstdint>
+#include <mutex>
+#include <set>
+
+namespace icinga
+{
+
+class JsonRpcConnection;
+
+/**
+ * @ingroup remote
+ */
+struct ConfigDirInformation
+{
+ Dictionary::Ptr UpdateV1;
+ Dictionary::Ptr UpdateV2;
+ Dictionary::Ptr Checksums;
+};
+
+/**
+ * If the version reported by icinga::Hello is not enough to tell whether
+ * the peer has a specific capability, add the latter to this bitmask.
+ *
+ * Note that due to the capability exchange via JSON-RPC and the state storage via JSON
+ * the bitmask numbers are stored in IEEE 754 64-bit floats.
+ * The latter have 53 digit bits which limit the bitmask.
+ * Not to run out of bits:
+ *
+ * Once all Icinga versions which don't have a specific capability are completely EOL,
+ * remove the respective capability checks and assume the peer has the capability.
+ * Once all Icinga versions which still check for the capability are completely EOL,
+ * remove the respective bit from icinga::Hello.
+ * Once all Icinga versions which still have the respective bit in icinga::Hello
+ * are completely EOL, remove the bit here.
+ * Once all Icinga versions which still have the respective bit here
+ * are completely EOL, feel free to re-use the bit.
+ *
+ * completely EOL = not supported, even if an important customer of us used it and
+ * not expected to appear in a multi-level cluster, e.g. a 4 level cluster with
+ * v2.11 -> v2.10 -> v2.9 -> v2.8 - v2.7 isn't here
+ *
+ * @ingroup remote
+ */
+enum class ApiCapabilities : uint_fast64_t
+{
+ ExecuteArbitraryCommand = 1u << 0u,
+ IfwApiCheckCommand = 1u << 1u,
+};
+
+/**
+* @ingroup remote
+*/
+class ApiListener final : public ObjectImpl<ApiListener>
+{
+public:
+ DECLARE_OBJECT(ApiListener);
+ DECLARE_OBJECTNAME(ApiListener);
+
+ static boost::signals2::signal<void(bool)> OnMasterChanged;
+
+ ApiListener();
+
+ static String GetApiDir();
+ static String GetApiZonesDir();
+ static String GetApiZonesStageDir();
+ static String GetCertsDir();
+ static String GetCaDir();
+ static String GetCertificateRequestsDir();
+
+ std::shared_ptr<X509> RenewCert(const std::shared_ptr<X509>& cert, bool ca = false);
+ void UpdateSSLContext();
+
+ static ApiListener::Ptr GetInstance();
+
+ Endpoint::Ptr GetMaster() const;
+ bool IsMaster() const;
+
+ Endpoint::Ptr GetLocalEndpoint() const;
+
+ void SyncSendMessage(const Endpoint::Ptr& endpoint, const Dictionary::Ptr& message);
+ void RelayMessage(const MessageOrigin::Ptr& origin, const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log);
+
+ static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
+ std::pair<Dictionary::Ptr, Dictionary::Ptr> GetStatus();
+
+ bool AddAnonymousClient(const JsonRpcConnection::Ptr& aclient);
+ void RemoveAnonymousClient(const JsonRpcConnection::Ptr& aclient);
+ std::set<JsonRpcConnection::Ptr> GetAnonymousClients() const;
+
+ void AddHttpClient(const HttpServerConnection::Ptr& aclient);
+ void RemoveHttpClient(const HttpServerConnection::Ptr& aclient);
+ std::set<HttpServerConnection::Ptr> GetHttpClients() const;
+
+ static double CalculateZoneLag(const Endpoint::Ptr& endpoint);
+
+ /* filesync */
+ static Value ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ void HandleConfigUpdate(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ /* configsync */
+ static void ConfigUpdateObjectHandler(const ConfigObject::Ptr& object, const Value& cookie);
+ static Value ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static Value ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ /* API config packages */
+ void SetActivePackageStage(const String& package, const String& stage);
+ String GetActivePackageStage(const String& package);
+ void RemoveActivePackageStage(const String& package);
+
+ static Value HelloAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
+ static void UpdateObjectAuthority();
+
+ static bool IsHACluster();
+ static String GetFromZoneName(const Zone::Ptr& fromZone);
+
+ static String GetDefaultCertPath();
+ static String GetDefaultKeyPath();
+ static String GetDefaultCaPath();
+
+ static inline
+ bool UpdatedObjectAuthority()
+ {
+ return m_UpdatedObjectAuthority.load();
+ }
+
+ double GetTlsHandshakeTimeout() const override;
+ void SetTlsHandshakeTimeout(double value, bool suppress_events, const Value& cookie) override;
+
+protected:
+ void OnConfigLoaded() override;
+ void OnAllConfigLoaded() override;
+ void Start(bool runtimeCreated) override;
+ void Stop(bool runtimeDeleted) override;
+
+ void ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
+ void ValidateTlsHandshakeTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ Shared<boost::asio::ssl::context>::Ptr m_SSLContext;
+ boost::shared_mutex m_SSLContextMutex;
+
+ mutable std::mutex m_AnonymousClientsLock;
+ mutable std::mutex m_HttpClientsLock;
+ std::set<JsonRpcConnection::Ptr> m_AnonymousClients;
+ std::set<HttpServerConnection::Ptr> m_HttpClients;
+
+ Timer::Ptr m_Timer;
+ Timer::Ptr m_ReconnectTimer;
+ Timer::Ptr m_AuthorityTimer;
+ Timer::Ptr m_CleanupCertificateRequestsTimer;
+ Timer::Ptr m_ApiPackageIntegrityTimer;
+ Timer::Ptr m_RenewOwnCertTimer;
+
+ Endpoint::Ptr m_LocalEndpoint;
+
+ static ApiListener::Ptr m_Instance;
+ static std::atomic<bool> m_UpdatedObjectAuthority;
+
+ void ApiTimerHandler();
+ void ApiReconnectTimerHandler();
+ void CleanupCertificateRequestsTimerHandler();
+ void CheckApiPackageIntegrity();
+
+ bool AddListener(const String& node, const String& service);
+ void AddConnection(const Endpoint::Ptr& endpoint);
+
+ void NewClientHandler(
+ boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
+ const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
+ );
+ void NewClientHandlerInternal(
+ boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
+ const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
+ );
+ void ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server);
+
+ WorkQueue m_RelayQueue;
+ WorkQueue m_SyncQueue{0, 4};
+
+ std::mutex m_LogLock;
+ Stream::Ptr m_LogFile;
+ size_t m_LogMessageCount{0};
+
+ bool RelayMessageOne(const Zone::Ptr& zone, const MessageOrigin::Ptr& origin, const Dictionary::Ptr& message, const Endpoint::Ptr& currentZoneMaster);
+ void SyncRelayMessage(const MessageOrigin::Ptr& origin, const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log);
+ void PersistMessage(const Dictionary::Ptr& message, const ConfigObject::Ptr& secobj);
+
+ void OpenLogFile();
+ void RotateLogFile();
+ void CloseLogFile();
+ static void LogGlobHandler(std::vector<int>& files, const String& file);
+ void ReplayLog(const JsonRpcConnection::Ptr& client);
+
+ static void CopyCertificateFile(const String& oldCertPath, const String& newCertPath);
+
+ void UpdateStatusFile(boost::asio::ip::tcp::endpoint localEndpoint);
+ void RemoveStatusFile();
+
+ /* filesync */
+ static std::mutex m_ConfigSyncStageLock;
+
+ void SyncLocalZoneDirs() const;
+ void SyncLocalZoneDir(const Zone::Ptr& zone) const;
+ void RenewOwnCert();
+ void RenewCA();
+
+ void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient);
+
+ static Dictionary::Ptr MergeConfigUpdate(const ConfigDirInformation& config);
+
+ static ConfigDirInformation LoadConfigDir(const String& dir);
+ static void ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file);
+
+ static void TryActivateZonesStage(const std::vector<String>& relativePaths);
+
+ static String GetChecksum(const String& content);
+ static bool CheckConfigChange(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig);
+
+ void UpdateLastFailedZonesStageValidation(const String& log);
+ void ClearLastFailedZonesStageValidation();
+
+ /* configsync */
+ void UpdateConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
+ const JsonRpcConnection::Ptr& client = nullptr);
+ void DeleteConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
+ const JsonRpcConnection::Ptr& client = nullptr);
+ void SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient);
+
+ void SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoint::Ptr& endpoint, bool needSync);
+
+ /* API Config Packages */
+ mutable std::mutex m_ActivePackageStagesLock;
+ std::map<String, String> m_ActivePackageStages;
+
+ void UpdateActivePackageStagesCache();
+};
+
+}
+
+#endif /* APILISTENER_H */
diff --git a/lib/remote/apilistener.ti b/lib/remote/apilistener.ti
new file mode 100644
index 0000000..8317abc
--- /dev/null
+++ b/lib/remote/apilistener.ti
@@ -0,0 +1,66 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/i2-remote.hpp"
+#include "base/configobject.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+
+library remote;
+
+namespace icinga
+{
+
+class ApiListener : ConfigObject
+{
+ activation_priority 50;
+
+ [config, deprecated] String cert_path;
+ [config, deprecated] String key_path;
+ [config, deprecated] String ca_path;
+ [config] String crl_path;
+ [config] String cipher_list {
+ default {{{ return DEFAULT_TLS_CIPHERS; }}}
+ };
+ [config] String tls_protocolmin {
+ default {{{ return DEFAULT_TLS_PROTOCOLMIN; }}}
+ };
+
+ [config] String bind_host {
+ default {{{ return Configuration::ApiBindHost; }}}
+ };
+ [config] String bind_port {
+ default {{{ return Configuration::ApiBindPort; }}}
+ };
+
+ [config] bool accept_config;
+ [config] bool accept_commands;
+ [config] int max_anonymous_clients {
+ default {{{ return -1; }}}
+ };
+
+ [config, deprecated] double tls_handshake_timeout {
+ get;
+ set;
+ default {{{ return Configuration::TlsHandshakeTimeout; }}}
+ };
+
+ [config] double connect_timeout {
+ default {{{ return DEFAULT_CONNECT_TIMEOUT; }}}
+ };
+
+ [config, no_user_view, no_user_modify] String ticket_salt;
+
+ [config] Array::Ptr access_control_allow_origin;
+ [config, deprecated] bool access_control_allow_credentials;
+ [config, deprecated] String access_control_allow_headers;
+ [config, deprecated] String access_control_allow_methods;
+
+
+ [state, no_user_modify] Timestamp log_message_timestamp;
+
+ [no_user_modify] String identity;
+
+ [state, no_user_modify] Dictionary::Ptr last_failed_zones_stage_validation;
+};
+
+}
diff --git a/lib/remote/apiuser.cpp b/lib/remote/apiuser.cpp
new file mode 100644
index 0000000..2959d89
--- /dev/null
+++ b/lib/remote/apiuser.cpp
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/apiuser.hpp"
+#include "remote/apiuser-ti.cpp"
+#include "base/configtype.hpp"
+#include "base/base64.hpp"
+#include "base/tlsutility.hpp"
+#include "base/utility.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(ApiUser);
+
+ApiUser::Ptr ApiUser::GetByClientCN(const String& cn)
+{
+ for (const ApiUser::Ptr& user : ConfigType::GetObjectsByType<ApiUser>()) {
+ if (user->GetClientCN() == cn)
+ return user;
+ }
+
+ return nullptr;
+}
+
+ApiUser::Ptr ApiUser::GetByAuthHeader(const String& auth_header)
+{
+ String::SizeType pos = auth_header.FindFirstOf(" ");
+ String username, password;
+
+ if (pos != String::NPos && auth_header.SubStr(0, pos) == "Basic") {
+ String credentials_base64 = auth_header.SubStr(pos + 1);
+ String credentials = Base64::Decode(credentials_base64);
+
+ String::SizeType cpos = credentials.FindFirstOf(":");
+
+ if (cpos != String::NPos) {
+ username = credentials.SubStr(0, cpos);
+ password = credentials.SubStr(cpos + 1);
+ }
+ }
+
+ const ApiUser::Ptr& user = ApiUser::GetByName(username);
+
+ /* Deny authentication if:
+ * 1) user does not exist
+ * 2) given password is empty
+ * 2) configured password does not match.
+ */
+ if (!user || password.IsEmpty())
+ return nullptr;
+ else if (user && !Utility::ComparePasswords(password, user->GetPassword()))
+ return nullptr;
+
+ return user;
+}
+
diff --git a/lib/remote/apiuser.hpp b/lib/remote/apiuser.hpp
new file mode 100644
index 0000000..fc132ee
--- /dev/null
+++ b/lib/remote/apiuser.hpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APIUSER_H
+#define APIUSER_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/apiuser-ti.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup remote
+ */
+class ApiUser final : public ObjectImpl<ApiUser>
+{
+public:
+ DECLARE_OBJECT(ApiUser);
+ DECLARE_OBJECTNAME(ApiUser);
+
+ static ApiUser::Ptr GetByClientCN(const String& cn);
+ static ApiUser::Ptr GetByAuthHeader(const String& auth_header);
+};
+
+}
+
+#endif /* APIUSER_H */
diff --git a/lib/remote/apiuser.ti b/lib/remote/apiuser.ti
new file mode 100644
index 0000000..0b49a1d
--- /dev/null
+++ b/lib/remote/apiuser.ti
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include "base/function.hpp"
+
+library remote;
+
+namespace icinga
+{
+
+class ApiUser : ConfigObject
+{
+ /* No show config */
+ [config, no_user_view] String password;
+ [deprecated, config, no_user_view] String password_hash;
+ [config] String client_cn (ClientCN);
+ [config] array(Value) permissions;
+};
+
+validator ApiUser {
+ Array permissions {
+ String "*";
+ Dictionary "*" {
+ required permission;
+ String permission;
+ Function filter;
+ };
+ };
+};
+
+}
diff --git a/lib/remote/configfileshandler.cpp b/lib/remote/configfileshandler.cpp
new file mode 100644
index 0000000..779ecd1
--- /dev/null
+++ b/lib/remote/configfileshandler.cpp
@@ -0,0 +1,94 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configfileshandler.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <fstream>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/config/files", ConfigFilesHandler);
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ const std::vector<String>& urlPath = url->GetPath();
+
+ if (urlPath.size() >= 4)
+ params->Set("package", urlPath[3]);
+
+ if (urlPath.size() >= 5)
+ params->Set("stage", urlPath[4]);
+
+ if (urlPath.size() >= 6) {
+ std::vector<String> tmpPath(urlPath.begin() + 5, urlPath.end());
+ params->Set("path", boost::algorithm::join(tmpPath, "/"));
+ }
+
+ if (request[http::field::accept] == "application/json") {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid Accept header. Either remove the Accept header or set it to 'application/octet-stream'.");
+ return true;
+ }
+
+ FilterUtility::CheckPermission(user, "config/query");
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+ String stageName = HttpUtility::GetLastParameter(params, "stage");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName)) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid package name.");
+ return true;
+ }
+
+ if (!ConfigPackageUtility::ValidateStageName(stageName)) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid stage name.");
+ return true;
+ }
+
+ String relativePath = HttpUtility::GetLastParameter(params, "path");
+
+ if (ConfigPackageUtility::ContainsDotDot(relativePath)) {
+ HttpUtility::SendJsonError(response, params, 400, "Path contains '..' (not allowed).");
+ return true;
+ }
+
+ String path = ConfigPackageUtility::GetPackageDir() + "/" + packageName + "/" + stageName + "/" + relativePath;
+
+ if (!Utility::PathExists(path)) {
+ HttpUtility::SendJsonError(response, params, 404, "Path not found.");
+ return true;
+ }
+
+ try {
+ std::ifstream fp(path.CStr(), std::ifstream::in | std::ifstream::binary);
+ fp.exceptions(std::ifstream::badbit);
+
+ String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+ response.result(http::status::ok);
+ response.set(http::field::content_type, "application/octet-stream");
+ response.body() = content;
+ response.content_length(response.body().size());
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 500, "Could not read file.",
+ DiagnosticInformation(ex));
+ }
+
+ return true;
+}
diff --git a/lib/remote/configfileshandler.hpp b/lib/remote/configfileshandler.hpp
new file mode 100644
index 0000000..ea48b1e
--- /dev/null
+++ b/lib/remote/configfileshandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGFILESHANDLER_H
+#define CONFIGFILESHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ConfigFilesHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigFilesHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* CONFIGFILESHANDLER_H */
diff --git a/lib/remote/configobjectslock.cpp b/lib/remote/configobjectslock.cpp
new file mode 100644
index 0000000..e529c83
--- /dev/null
+++ b/lib/remote/configobjectslock.cpp
@@ -0,0 +1,24 @@
+/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+#ifndef _WIN32
+
+#include "base/shared-memory.hpp"
+#include "remote/configobjectslock.hpp"
+#include <boost/interprocess/sync/lock_options.hpp>
+
+using namespace icinga;
+
+// On *nix one process may write config objects while another is loading the config, so this uses IPC.
+static SharedMemory<boost::interprocess::interprocess_sharable_mutex> l_ConfigObjectsMutex;
+
+ConfigObjectsExclusiveLock::ConfigObjectsExclusiveLock()
+ : m_Lock(l_ConfigObjectsMutex.Get())
+{
+}
+
+ConfigObjectsSharedLock::ConfigObjectsSharedLock(std::try_to_lock_t)
+ : m_Lock(l_ConfigObjectsMutex.Get(), boost::interprocess::try_to_lock)
+{
+}
+
+#endif /* _WIN32 */
diff --git a/lib/remote/configobjectslock.hpp b/lib/remote/configobjectslock.hpp
new file mode 100644
index 0000000..ee90981
--- /dev/null
+++ b/lib/remote/configobjectslock.hpp
@@ -0,0 +1,72 @@
+/* Icinga 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+#pragma once
+
+#include <mutex>
+
+#ifndef _WIN32
+#include <boost/interprocess/sync/interprocess_sharable_mutex.hpp>
+#include <boost/interprocess/sync/scoped_lock.hpp>
+#include <boost/interprocess/sync/sharable_lock.hpp>
+#endif /* _WIN32 */
+
+namespace icinga
+{
+
+#ifdef _WIN32
+
+class ConfigObjectsSharedLock
+{
+public:
+ inline ConfigObjectsSharedLock(std::try_to_lock_t)
+ {
+ }
+
+ constexpr explicit operator bool() const
+ {
+ return true;
+ }
+};
+
+#else /* _WIN32 */
+
+/**
+ * Waits until all ConfigObjects*Lock-s have vanished. For its lifetime disallows such.
+ * Keep an instance alive during reload to forbid runtime config changes!
+ * This way Icinga reads a consistent config which doesn't suddenly get runtime-changed.
+ *
+ * @ingroup remote
+ */
+class ConfigObjectsExclusiveLock
+{
+public:
+ ConfigObjectsExclusiveLock();
+
+private:
+ boost::interprocess::scoped_lock<boost::interprocess::interprocess_sharable_mutex> m_Lock;
+};
+
+/**
+ * Waits until the only ConfigObjectsExclusiveLock has vanished (if any). For its lifetime disallows such.
+ * Keep an instance alive during runtime config changes to delay a reload (if any)!
+ * This way Icinga reads a consistent config which doesn't suddenly get runtime-changed.
+ *
+ * @ingroup remote
+ */
+class ConfigObjectsSharedLock
+{
+public:
+ ConfigObjectsSharedLock(std::try_to_lock_t);
+
+ inline explicit operator bool() const
+ {
+ return m_Lock.owns();
+ }
+
+private:
+ boost::interprocess::sharable_lock<boost::interprocess::interprocess_sharable_mutex> m_Lock;
+};
+
+#endif /* _WIN32 */
+
+}
diff --git a/lib/remote/configobjectutility.cpp b/lib/remote/configobjectutility.cpp
new file mode 100644
index 0000000..62c910b
--- /dev/null
+++ b/lib/remote/configobjectutility.cpp
@@ -0,0 +1,377 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configobjectutility.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/apilistener.hpp"
+#include "config/configcompiler.hpp"
+#include "config/configitem.hpp"
+#include "base/configwriter.hpp"
+#include "base/exception.hpp"
+#include "base/dependencygraph.hpp"
+#include "base/tlsutility.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/system/error_code.hpp>
+#include <fstream>
+#include <utility>
+
+using namespace icinga;
+
+String ConfigObjectUtility::GetConfigDir()
+{
+ String prefix = ConfigPackageUtility::GetPackageDir() + "/_api/";
+ String activeStage = ConfigPackageUtility::GetActiveStage("_api");
+
+ if (activeStage.IsEmpty())
+ RepairPackage("_api");
+
+ return prefix + activeStage;
+}
+
+String ConfigObjectUtility::ComputeNewObjectConfigPath(const Type::Ptr& type, const String& fullName)
+{
+ String typeDir = type->GetPluralName();
+ boost::algorithm::to_lower(typeDir);
+
+ /* This may throw an exception the caller above must handle. */
+ String prefix = GetConfigDir() + "/conf.d/" + type->GetPluralName().ToLower() + "/";
+
+ String escapedName = EscapeName(fullName);
+
+ String longPath = prefix + escapedName + ".conf";
+
+ /*
+ * The long path may cause trouble due to exceeding the allowed filename length of the filesystem. Therefore, the
+ * preferred solution would be to use the truncated and hashed version as returned at the end of this function.
+ * However, for compatibility reasons, we have to keep the old long version in some cases. Notably, this could lead
+ * to the creation of objects that can't be synced to child nodes if they are running an older version. Thus, for
+ * now, the fix is only enabled for comments and downtimes, as these are the object types for which the issue is
+ * most likely triggered but can't be worked around easily (you'd have to rename the host and/or service in order to
+ * be able to schedule a downtime or add an acknowledgement, which is not feasible) and the impact of not syncing
+ * these objects through the whole cluster is limited. For other object types, we currently prefer to fail the
+ * creation early so that configuration inconsistencies throughout the cluster are avoided.
+ *
+ * TODO: Remove this in v2.16 and truncate all.
+ */
+ if (type->GetName() != "Comment" && type->GetName() != "Downtime") {
+ return longPath;
+ }
+
+ /* Maximum length 80 bytes object name + 3 bytes "..." + 40 bytes SHA1 (hex-encoded) */
+ return prefix + Utility::TruncateUsingHash<80+3+40>(escapedName) + ".conf";
+}
+
+String ConfigObjectUtility::GetExistingObjectConfigPath(const ConfigObject::Ptr& object)
+{
+ return object->GetDebugInfo().Path;
+}
+
+void ConfigObjectUtility::RepairPackage(const String& package)
+{
+ /* Try to fix the active stage, whenever we find a directory in there.
+ * This automatically heals packages < 2.11 which remained broken.
+ */
+ String dir = ConfigPackageUtility::GetPackageDir() + "/" + package + "/";
+
+ namespace fs = boost::filesystem;
+
+ /* Use iterators to workaround VS builds on Windows. */
+ fs::path path(dir.Begin(), dir.End());
+
+ fs::recursive_directory_iterator end;
+
+ String foundActiveStage;
+
+ for (fs::recursive_directory_iterator it(path); it != end; ++it) {
+ boost::system::error_code ec;
+
+ const fs::path d = *it;
+ if (fs::is_directory(d, ec)) {
+ /* Extract the relative directory name. */
+ foundActiveStage = d.stem().string();
+
+ break; // Use the first found directory.
+ }
+ }
+
+ if (!foundActiveStage.IsEmpty()) {
+ Log(LogInformation, "ConfigObjectUtility")
+ << "Repairing config package '" << package << "' with stage '" << foundActiveStage << "'.";
+
+ ConfigPackageUtility::ActivateStage(package, foundActiveStage);
+ } else {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot repair package '" + package + "', please check the troubleshooting docs."));
+ }
+}
+
+void ConfigObjectUtility::CreateStorage()
+{
+ std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
+
+ /* For now, we only use _api as our creation target. */
+ String package = "_api";
+
+ if (!ConfigPackageUtility::PackageExists(package)) {
+ Log(LogNotice, "ConfigObjectUtility")
+ << "Package " << package << " doesn't exist yet, creating it.";
+
+ ConfigPackageUtility::CreatePackage(package);
+
+ String stage = ConfigPackageUtility::CreateStage(package);
+ ConfigPackageUtility::ActivateStage(package, stage);
+ }
+}
+
+String ConfigObjectUtility::EscapeName(const String& name)
+{
+ return Utility::EscapeString(name, "<>:\"/\\|?*", true);
+}
+
+String ConfigObjectUtility::CreateObjectConfig(const Type::Ptr& type, const String& fullName,
+ bool ignoreOnError, const Array::Ptr& templates, const Dictionary::Ptr& attrs)
+{
+ auto *nc = dynamic_cast<NameComposer *>(type.get());
+ Dictionary::Ptr nameParts;
+ String name;
+
+ if (nc) {
+ nameParts = nc->ParseName(fullName);
+ name = nameParts->Get("name");
+ } else
+ name = fullName;
+
+ Dictionary::Ptr allAttrs = new Dictionary();
+
+ if (attrs) {
+ attrs->CopyTo(allAttrs);
+
+ ObjectLock olock(attrs);
+ for (const Dictionary::Pair& kv : attrs) {
+ int fid = type->GetFieldId(kv.first.SubStr(0, kv.first.FindFirstOf(".")));
+
+ if (fid < 0)
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid attribute specified: " + kv.first));
+
+ Field field = type->GetFieldInfo(fid);
+
+ if (!(field.Attributes & FAConfig) || kv.first == "name")
+ BOOST_THROW_EXCEPTION(ScriptError("Attribute is marked for internal use only and may not be set: " + kv.first));
+ }
+ }
+
+ if (nameParts)
+ nameParts->CopyTo(allAttrs);
+
+ allAttrs->Remove("name");
+
+ /* update the version for config sync */
+ allAttrs->Set("version", Utility::GetTime());
+
+ std::ostringstream config;
+ ConfigWriter::EmitConfigItem(config, type->GetName(), name, false, ignoreOnError, templates, allAttrs);
+ ConfigWriter::EmitRaw(config, "\n");
+
+ return config.str();
+}
+
+bool ConfigObjectUtility::CreateObject(const Type::Ptr& type, const String& fullName,
+ const String& config, const Array::Ptr& errors, const Array::Ptr& diagnosticInformation, const Value& cookie)
+{
+ CreateStorage();
+
+ {
+ auto configType (dynamic_cast<ConfigType*>(type.get()));
+
+ if (configType && configType->GetObject(fullName)) {
+ errors->Add("Object '" + fullName + "' already exists.");
+ return false;
+ }
+ }
+
+ String path;
+
+ try {
+ path = ComputeNewObjectConfigPath(type, fullName);
+ } catch (const std::exception& ex) {
+ errors->Add("Config package broken: " + DiagnosticInformation(ex, false));
+ return false;
+ }
+
+ Utility::MkDirP(Utility::DirName(path), 0700);
+
+ std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::trunc);
+ fp << config;
+ fp.close();
+
+ std::unique_ptr<Expression> expr = ConfigCompiler::CompileFile(path, String(), "_api");
+
+ try {
+ ActivationScope ascope;
+
+ ScriptFrame frame(true);
+ expr->Evaluate(frame);
+ expr.reset();
+
+ WorkQueue upq;
+ upq.SetName("ConfigObjectUtility::CreateObject");
+
+ std::vector<ConfigItem::Ptr> newItems;
+
+ /*
+ * Disable logging for object creation, but do so ourselves later on.
+ * Duplicate the error handling for better logging and debugging here.
+ */
+ if (!ConfigItem::CommitItems(ascope.GetContext(), upq, newItems, true)) {
+ if (errors) {
+ Log(LogNotice, "ConfigObjectUtility")
+ << "Failed to commit config item '" << fullName << "'. Aborting and removing config path '" << path << "'.";
+
+ Utility::Remove(path);
+
+ for (const boost::exception_ptr& ex : upq.GetExceptions()) {
+ errors->Add(DiagnosticInformation(ex, false));
+
+ if (diagnosticInformation)
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+ }
+ }
+
+ return false;
+ }
+
+ /*
+ * Activate the config object.
+ * uq, items, runtimeCreated, silent, withModAttrs, cookie
+ * IMPORTANT: Forward the cookie aka origin in order to prevent sync loops in the same zone!
+ */
+ if (!ConfigItem::ActivateItems(newItems, true, false, false, cookie)) {
+ if (errors) {
+ Log(LogNotice, "ConfigObjectUtility")
+ << "Failed to activate config object '" << fullName << "'. Aborting and removing config path '" << path << "'.";
+
+ Utility::Remove(path);
+
+ for (const boost::exception_ptr& ex : upq.GetExceptions()) {
+ errors->Add(DiagnosticInformation(ex, false));
+
+ if (diagnosticInformation)
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+ }
+ }
+
+ return false;
+ }
+
+ /* if (type != Comment::TypeInstance && type != Downtime::TypeInstance)
+ * Does not work since this would require libicinga, which has a dependency on libremote
+ * Would work if these libs were static.
+ */
+ if (type->GetName() != "Comment" && type->GetName() != "Downtime")
+ ApiListener::UpdateObjectAuthority();
+
+ // At this stage we should have a config object already. If not, it was ignored before.
+ auto *ctype = dynamic_cast<ConfigType *>(type.get());
+ ConfigObject::Ptr obj = ctype->GetObject(fullName);
+
+ if (obj) {
+ Log(LogInformation, "ConfigObjectUtility")
+ << "Created and activated object '" << fullName << "' of type '" << type->GetName() << "'.";
+ } else {
+ Log(LogNotice, "ConfigObjectUtility")
+ << "Object '" << fullName << "' was not created but ignored due to errors.";
+ }
+
+ } catch (const std::exception& ex) {
+ Utility::Remove(path);
+
+ if (errors)
+ errors->Add(DiagnosticInformation(ex, false));
+
+ if (diagnosticInformation)
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+
+ return false;
+ }
+
+ return true;
+}
+
+bool ConfigObjectUtility::DeleteObjectHelper(const ConfigObject::Ptr& object, bool cascade,
+ const Array::Ptr& errors, const Array::Ptr& diagnosticInformation, const Value& cookie)
+{
+ std::vector<Object::Ptr> parents = DependencyGraph::GetParents(object);
+
+ Type::Ptr type = object->GetReflectionType();
+
+ String name = object->GetName();
+
+ if (!parents.empty() && !cascade) {
+ if (errors) {
+ errors->Add("Object '" + name + "' of type '" + type->GetName() +
+ "' cannot be deleted because other objects depend on it. "
+ "Use cascading delete to delete it anyway.");
+ }
+
+ return false;
+ }
+
+ for (const Object::Ptr& pobj : parents) {
+ ConfigObject::Ptr parentObj = dynamic_pointer_cast<ConfigObject>(pobj);
+
+ if (!parentObj)
+ continue;
+
+ DeleteObjectHelper(parentObj, cascade, errors, diagnosticInformation, cookie);
+ }
+
+ ConfigItem::Ptr item = ConfigItem::GetByTypeAndName(type, name);
+
+ try {
+ /* mark this object for cluster delete event */
+ object->SetExtension("ConfigObjectDeleted", true);
+
+ /*
+ * Trigger deactivation signal for DB IDO and runtime object delections.
+ * IMPORTANT: Specify the cookie aka origin in order to prevent sync loops
+ * in the same zone!
+ */
+ object->Deactivate(true, cookie);
+
+ if (item)
+ item->Unregister();
+ else
+ object->Unregister();
+
+ } catch (const std::exception& ex) {
+ if (errors)
+ errors->Add(DiagnosticInformation(ex, false));
+
+ if (diagnosticInformation)
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+
+ return false;
+ }
+
+ if (object->GetPackage() == "_api") {
+ Utility::Remove(GetExistingObjectConfigPath(object));
+ }
+
+ Log(LogInformation, "ConfigObjectUtility")
+ << "Deleted object '" << name << "' of type '" << type->GetName() << "'.";
+
+ return true;
+}
+
+bool ConfigObjectUtility::DeleteObject(const ConfigObject::Ptr& object, bool cascade, const Array::Ptr& errors,
+ const Array::Ptr& diagnosticInformation, const Value& cookie)
+{
+ if (object->GetPackage() != "_api") {
+ if (errors)
+ errors->Add("Object cannot be deleted because it was not created using the API.");
+
+ return false;
+ }
+
+ return DeleteObjectHelper(object, cascade, errors, diagnosticInformation, cookie);
+}
diff --git a/lib/remote/configobjectutility.hpp b/lib/remote/configobjectutility.hpp
new file mode 100644
index 0000000..5a113c8
--- /dev/null
+++ b/lib/remote/configobjectutility.hpp
@@ -0,0 +1,47 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGOBJECTUTILITY_H
+#define CONFIGOBJECTUTILITY_H
+
+#include "remote/i2-remote.hpp"
+#include "base/array.hpp"
+#include "base/configobject.hpp"
+#include "base/dictionary.hpp"
+#include "base/type.hpp"
+
+namespace icinga
+{
+
+/**
+ * Helper functions.
+ *
+ * @ingroup remote
+ */
+class ConfigObjectUtility
+{
+
+public:
+ static String GetConfigDir();
+ static String ComputeNewObjectConfigPath(const Type::Ptr& type, const String& fullName);
+ static String GetExistingObjectConfigPath(const ConfigObject::Ptr& object);
+ static void RepairPackage(const String& package);
+ static void CreateStorage();
+
+ static String CreateObjectConfig(const Type::Ptr& type, const String& fullName,
+ bool ignoreOnError, const Array::Ptr& templates, const Dictionary::Ptr& attrs);
+
+ static bool CreateObject(const Type::Ptr& type, const String& fullName,
+ const String& config, const Array::Ptr& errors, const Array::Ptr& diagnosticInformation, const Value& cookie = Empty);
+
+ static bool DeleteObject(const ConfigObject::Ptr& object, bool cascade, const Array::Ptr& errors,
+ const Array::Ptr& diagnosticInformation, const Value& cookie = Empty);
+
+private:
+ static String EscapeName(const String& name);
+ static bool DeleteObjectHelper(const ConfigObject::Ptr& object, bool cascade, const Array::Ptr& errors,
+ const Array::Ptr& diagnosticInformation, const Value& cookie = Empty);
+};
+
+}
+
+#endif /* CONFIGOBJECTUTILITY_H */
diff --git a/lib/remote/configpackageshandler.cpp b/lib/remote/configpackageshandler.cpp
new file mode 100644
index 0000000..98b3268
--- /dev/null
+++ b/lib/remote/configpackageshandler.cpp
@@ -0,0 +1,179 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configpackageshandler.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/config/packages", ConfigPackagesHandler);
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() == http::verb::get)
+ HandleGet(user, request, url, response, params);
+ else if (request.method() == http::verb::post)
+ HandlePost(user, request, url, response, params);
+ else if (request.method() == http::verb::delete_)
+ HandleDelete(user, request, url, response, params);
+ else
+ return false;
+
+ return true;
+}
+
+void ConfigPackagesHandler::HandleGet(
+ const ApiUser::Ptr& user,
+ 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");
+
+ std::vector<String> packages;
+
+ try {
+ packages = ConfigPackageUtility::GetPackages();
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 500, "Could not retrieve packages.",
+ DiagnosticInformation(ex));
+ return;
+ }
+
+ ArrayData results;
+
+ {
+ std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
+
+ for (const String& package : packages) {
+ String activeStage;
+
+ try {
+ activeStage = ConfigPackageUtility::GetActiveStage(package);
+ } catch (const std::exception&) { } /* Should never happen. */
+
+ results.emplace_back(new Dictionary({
+ { "name", package },
+ { "stages", Array::FromVector(ConfigPackageUtility::GetStages(package)) },
+ { "active-stage", activeStage }
+ }));
+ }
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
+void ConfigPackagesHandler::HandlePost(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/modify");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName)) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+ return;
+ }
+
+ try {
+ std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
+
+ ConfigPackageUtility::CreatePackage(packageName);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 500, "Could not create package '" + packageName + "'.",
+ DiagnosticInformation(ex));
+ return;
+ }
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "code", 200 },
+ { "package", packageName },
+ { "status", "Created package." }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
+void ConfigPackagesHandler::HandleDelete(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/modify");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName)) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+ return;
+ }
+
+ try {
+ ConfigPackageUtility::DeletePackage(packageName);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 500, "Failed to delete package '" + packageName + "'.",
+ DiagnosticInformation(ex));
+ return;
+ }
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "code", 200 },
+ { "package", packageName },
+ { "status", "Deleted package." }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
diff --git a/lib/remote/configpackageshandler.hpp b/lib/remote/configpackageshandler.hpp
new file mode 100644
index 0000000..0a05ea1
--- /dev/null
+++ b/lib/remote/configpackageshandler.hpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGMODULESHANDLER_H
+#define CONFIGMODULESHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ConfigPackagesHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigPackagesHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+
+private:
+ void HandleGet(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+ void HandlePost(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+ void HandleDelete(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+
+};
+
+}
+
+#endif /* CONFIGMODULESHANDLER_H */
diff --git a/lib/remote/configpackageutility.cpp b/lib/remote/configpackageutility.cpp
new file mode 100644
index 0000000..e795401
--- /dev/null
+++ b/lib/remote/configpackageutility.cpp
@@ -0,0 +1,413 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configpackageutility.hpp"
+#include "remote/apilistener.hpp"
+#include "base/application.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string.hpp>
+#include <boost/regex.hpp>
+#include <algorithm>
+#include <cctype>
+#include <fstream>
+
+using namespace icinga;
+
+String ConfigPackageUtility::GetPackageDir()
+{
+ return Configuration::DataDir + "/api/packages";
+}
+
+void ConfigPackageUtility::CreatePackage(const String& name)
+{
+ String path = GetPackageDir() + "/" + name;
+
+ if (Utility::PathExists(path))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Package already exists."));
+
+ Utility::MkDirP(path, 0700);
+ WritePackageConfig(name);
+}
+
+void ConfigPackageUtility::DeletePackage(const String& name)
+{
+ String path = GetPackageDir() + "/" + name;
+
+ if (!Utility::PathExists(path))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Package does not exist."));
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ /* config packages without API make no sense. */
+ if (!listener)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("No ApiListener instance configured."));
+
+ listener->RemoveActivePackageStage(name);
+
+ Utility::RemoveDirRecursive(path);
+ Application::RequestRestart();
+}
+
+std::vector<String> ConfigPackageUtility::GetPackages()
+{
+ String packageDir = GetPackageDir();
+
+ std::vector<String> packages;
+
+ /* Package directory does not exist, no packages have been created thus far. */
+ if (!Utility::PathExists(packageDir))
+ return packages;
+
+ Utility::Glob(packageDir + "/*", [&packages](const String& path) { packages.emplace_back(Utility::BaseName(path)); }, GlobDirectory);
+
+ return packages;
+}
+
+bool ConfigPackageUtility::PackageExists(const String& name)
+{
+ auto packages (GetPackages());
+ return std::find(packages.begin(), packages.end(), name) != packages.end();
+}
+
+String ConfigPackageUtility::CreateStage(const String& packageName, const Dictionary::Ptr& files)
+{
+ String stageName = Utility::NewUniqueID();
+
+ String path = GetPackageDir() + "/" + packageName;
+
+ if (!Utility::PathExists(path))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Package does not exist."));
+
+ path += "/" + stageName;
+
+ Utility::MkDirP(path, 0700);
+ Utility::MkDirP(path + "/conf.d", 0700);
+ Utility::MkDirP(path + "/zones.d", 0700);
+ WriteStageConfig(packageName, stageName);
+
+ bool foundDotDot = false;
+
+ if (files) {
+ ObjectLock olock(files);
+ for (const Dictionary::Pair& kv : files) {
+ if (ContainsDotDot(kv.first)) {
+ foundDotDot = true;
+ break;
+ }
+
+ String filePath = path + "/" + kv.first;
+
+ Log(LogInformation, "ConfigPackageUtility")
+ << "Updating configuration file: " << filePath;
+
+ // Pass the directory and generate a dir tree, if it does not already exist
+ Utility::MkDirP(Utility::DirName(filePath), 0750);
+ std::ofstream fp(filePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fp << kv.second;
+ fp.close();
+ }
+ }
+
+ if (foundDotDot) {
+ Utility::RemoveDirRecursive(path);
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Path must not contain '..'."));
+ }
+
+ return stageName;
+}
+
+void ConfigPackageUtility::WritePackageConfig(const String& packageName)
+{
+ String stageName = GetActiveStage(packageName);
+
+ String includePath = GetPackageDir() + "/" + packageName + "/include.conf";
+ std::ofstream fpInclude(includePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpInclude << "include \"*/include.conf\"\n";
+ fpInclude.close();
+
+ String activePath = GetPackageDir() + "/" + packageName + "/active.conf";
+ std::ofstream fpActive(activePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpActive << "if (!globals.contains(\"ActiveStages\")) {\n"
+ << " globals.ActiveStages = {}\n"
+ << "}\n"
+ << "\n"
+ << "if (globals.contains(\"ActiveStageOverride\")) {\n"
+ << " var arr = ActiveStageOverride.split(\":\")\n"
+ << " if (arr[0] == \"" << packageName << "\") {\n"
+ << " if (arr.len() < 2) {\n"
+ << " log(LogCritical, \"Config\", \"Invalid value for ActiveStageOverride\")\n"
+ << " } else {\n"
+ << " ActiveStages[\"" << packageName << "\"] = arr[1]\n"
+ << " }\n"
+ << " }\n"
+ << "}\n"
+ << "\n"
+ << "if (!ActiveStages.contains(\"" << packageName << "\")) {\n"
+ << " ActiveStages[\"" << packageName << "\"] = \"" << stageName << "\"\n"
+ << "}\n";
+ fpActive.close();
+}
+
+void ConfigPackageUtility::WriteStageConfig(const String& packageName, const String& stageName)
+{
+ String path = GetPackageDir() + "/" + packageName + "/" + stageName + "/include.conf";
+ std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fp << "include \"../active.conf\"\n"
+ << "if (ActiveStages[\"" << packageName << "\"] == \"" << stageName << "\") {\n"
+ << " include_recursive \"conf.d\"\n"
+ << " include_zones \"" << packageName << "\", \"zones.d\"\n"
+ << "}\n";
+ fp.close();
+}
+
+void ConfigPackageUtility::ActivateStage(const String& packageName, const String& stageName)
+{
+ SetActiveStage(packageName, stageName);
+
+ WritePackageConfig(packageName);
+}
+
+void ConfigPackageUtility::TryActivateStageCallback(const ProcessResult& pr, const String& packageName, const String& stageName,
+ bool activate, bool reload, const Shared<Defer>::Ptr& resetPackageUpdates)
+{
+ String logFile = GetPackageDir() + "/" + packageName + "/" + stageName + "/startup.log";
+ std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpLog << pr.Output;
+ fpLog.close();
+
+ String statusFile = GetPackageDir() + "/" + packageName + "/" + stageName + "/status";
+ std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
+ fpStatus << pr.ExitStatus;
+ fpStatus.close();
+
+ /* validation went fine, activate stage and reload */
+ if (pr.ExitStatus == 0) {
+ if (activate) {
+ {
+ std::unique_lock<std::mutex> lock(GetStaticPackageMutex());
+
+ ActivateStage(packageName, stageName);
+ }
+
+ if (reload) {
+ /*
+ * Cancel the deferred callback before going out of scope so that the config stages handler
+ * flag isn't resetting earlier and allowing other clients to submit further requests while
+ * Icinga2 is reloading. Otherwise, the ongoing request will be cancelled halfway before the
+ * operation is completed once the new worker becomes ready.
+ */
+ resetPackageUpdates->Cancel();
+
+ Application::RequestRestart();
+ }
+ }
+ } else {
+ Log(LogCritical, "ConfigPackageUtility")
+ << "Config validation failed for package '"
+ << packageName << "' and stage '" << stageName << "'.";
+ }
+}
+
+void ConfigPackageUtility::AsyncTryActivateStage(const String& packageName, const String& stageName, bool activate, bool reload,
+ const Shared<Defer>::Ptr& resetPackageUpdates)
+{
+ VERIFY(Application::GetArgC() >= 1);
+
+ // prepare arguments
+ Array::Ptr args = new Array({
+ Application::GetExePath(Application::GetArgV()[0]),
+ });
+
+ // copy all arguments of parent process
+ for (int i = 1; i < Application::GetArgC(); i++) {
+ String argV = Application::GetArgV()[i];
+
+ if (argV == "-d" || argV == "--daemonize")
+ continue;
+
+ args->Add(argV);
+ }
+
+ // add arguments for validation
+ args->Add("--validate");
+ args->Add("--define");
+ args->Add("ActiveStageOverride=" + packageName + ":" + stageName);
+
+ Process::Ptr process = new Process(Process::PrepareCommand(args));
+ process->SetTimeout(Application::GetReloadTimeout());
+ process->Run([packageName, stageName, activate, reload, resetPackageUpdates](const ProcessResult& pr) {
+ TryActivateStageCallback(pr, packageName, stageName, activate, reload, resetPackageUpdates);
+ });
+}
+
+void ConfigPackageUtility::DeleteStage(const String& packageName, const String& stageName)
+{
+ String path = GetPackageDir() + "/" + packageName + "/" + stageName;
+
+ if (!Utility::PathExists(path))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Stage does not exist."));
+
+ if (GetActiveStage(packageName) == stageName)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Active stage cannot be deleted."));
+
+ Utility::RemoveDirRecursive(path);
+}
+
+std::vector<String> ConfigPackageUtility::GetStages(const String& packageName)
+{
+ std::vector<String> stages;
+ Utility::Glob(GetPackageDir() + "/" + packageName + "/*", [&stages](const String& path) { stages.emplace_back(Utility::BaseName(path)); }, GlobDirectory);
+ return stages;
+}
+
+String ConfigPackageUtility::GetActiveStageFromFile(const String& packageName)
+{
+ /* Lock the transaction, reading this only happens on startup or when something really is broken. */
+ std::unique_lock<std::mutex> lock(GetStaticActiveStageMutex());
+
+ String path = GetPackageDir() + "/" + packageName + "/active-stage";
+
+ std::ifstream fp;
+ fp.open(path.CStr());
+
+ String stage;
+ std::getline(fp, stage.GetData());
+
+ fp.close();
+
+ if (fp.fail())
+ return ""; /* Don't use exceptions here. The caller must deal with empty stages at this point. Happens on initial package creation for example. */
+
+ return stage.Trim();
+}
+
+void ConfigPackageUtility::SetActiveStageToFile(const String& packageName, const String& stageName)
+{
+ std::unique_lock<std::mutex> lock(GetStaticActiveStageMutex());
+
+ String activeStagePath = GetPackageDir() + "/" + packageName + "/active-stage";
+
+ std::ofstream fpActiveStage(activeStagePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); //TODO: fstream exceptions
+ fpActiveStage << stageName;
+ fpActiveStage.close();
+}
+
+String ConfigPackageUtility::GetActiveStage(const String& packageName)
+{
+ String activeStage;
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ /* If we don't have an API feature, just use the file storage without caching this.
+ * This happens when ScheduledDowntime objects generate Downtime objects.
+ * TODO: Make the API a first class citizen.
+ */
+ if (!listener)
+ return GetActiveStageFromFile(packageName);
+
+ /* First use runtime state. */
+ try {
+ activeStage = listener->GetActivePackageStage(packageName);
+ } catch (const std::exception& ex) {
+ /* Fallback to reading the file, happens on restarts. */
+ activeStage = GetActiveStageFromFile(packageName);
+
+ /* When we've read something, correct memory. */
+ if (!activeStage.IsEmpty())
+ listener->SetActivePackageStage(packageName, activeStage);
+ }
+
+ return activeStage;
+}
+
+void ConfigPackageUtility::SetActiveStage(const String& packageName, const String& stageName)
+{
+ /* Update the marker on disk for restarts. */
+ SetActiveStageToFile(packageName, stageName);
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ /* No API, no caching. */
+ if (!listener)
+ return;
+
+ listener->SetActivePackageStage(packageName, stageName);
+}
+
+std::vector<std::pair<String, bool> > ConfigPackageUtility::GetFiles(const String& packageName, const String& stageName)
+{
+ std::vector<std::pair<String, bool> > paths;
+ Utility::GlobRecursive(GetPackageDir() + "/" + packageName + "/" + stageName, "*", [&paths](const String& path) {
+ CollectPaths(path, paths);
+ }, GlobDirectory | GlobFile);
+
+ return paths;
+}
+
+void ConfigPackageUtility::CollectPaths(const String& path, std::vector<std::pair<String, bool> >& paths)
+{
+#ifndef _WIN32
+ struct stat statbuf;
+ int rc = lstat(path.CStr(), &statbuf);
+ if (rc < 0)
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("lstat")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(path));
+
+ paths.emplace_back(path, S_ISDIR(statbuf.st_mode));
+#else /* _WIN32 */
+ struct _stat statbuf;
+ int rc = _stat(path.CStr(), &statbuf);
+ if (rc < 0)
+ BOOST_THROW_EXCEPTION(posix_error()
+ << boost::errinfo_api_function("_stat")
+ << boost::errinfo_errno(errno)
+ << boost::errinfo_file_name(path));
+
+ paths.emplace_back(path, ((statbuf.st_mode & S_IFMT) == S_IFDIR));
+#endif /* _WIN32 */
+}
+
+bool ConfigPackageUtility::ContainsDotDot(const String& path)
+{
+ std::vector<String> tokens = path.Split("/\\");
+
+ for (const String& part : tokens) {
+ if (part == "..")
+ return true;
+ }
+
+ return false;
+}
+
+bool ConfigPackageUtility::ValidatePackageName(const String& packageName)
+{
+ return ValidateFreshName(packageName) || PackageExists(packageName);
+}
+
+bool ConfigPackageUtility::ValidateFreshName(const String& name)
+{
+ if (name.IsEmpty())
+ return false;
+
+ /* check for path injection */
+ if (ContainsDotDot(name))
+ return false;
+
+ return std::all_of(name.Begin(), name.End(), [](char c) {
+ return std::isalnum(c, std::locale::classic()) || c == '_' || c == '-';
+ });
+}
+
+std::mutex& ConfigPackageUtility::GetStaticPackageMutex()
+{
+ static std::mutex mutex;
+ return mutex;
+}
+
+std::mutex& ConfigPackageUtility::GetStaticActiveStageMutex()
+{
+ static std::mutex mutex;
+ return mutex;
+}
diff --git a/lib/remote/configpackageutility.hpp b/lib/remote/configpackageutility.hpp
new file mode 100644
index 0000000..240f591
--- /dev/null
+++ b/lib/remote/configpackageutility.hpp
@@ -0,0 +1,73 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGMODULEUTILITY_H
+#define CONFIGMODULEUTILITY_H
+
+#include "remote/i2-remote.hpp"
+#include "base/application.hpp"
+#include "base/dictionary.hpp"
+#include "base/process.hpp"
+#include "base/string.hpp"
+#include "base/defer.hpp"
+#include "base/shared.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * Helper functions.
+ *
+ * @ingroup remote
+ */
+class ConfigPackageUtility
+{
+
+public:
+ static String GetPackageDir();
+
+ static void CreatePackage(const String& name);
+ static void DeletePackage(const String& name);
+ static std::vector<String> GetPackages();
+ static bool PackageExists(const String& name);
+
+ static String CreateStage(const String& packageName, const Dictionary::Ptr& files = nullptr);
+ static void DeleteStage(const String& packageName, const String& stageName);
+ static std::vector<String> GetStages(const String& packageName);
+ static String GetActiveStageFromFile(const String& packageName);
+ static String GetActiveStage(const String& packageName);
+ static void SetActiveStage(const String& packageName, const String& stageName);
+ static void SetActiveStageToFile(const String& packageName, const String& stageName);
+ static void ActivateStage(const String& packageName, const String& stageName);
+ static void AsyncTryActivateStage(const String& packageName, const String& stageName, bool activate, bool reload,
+ const Shared<Defer>::Ptr& resetPackageUpdates);
+
+ static std::vector<std::pair<String, bool> > GetFiles(const String& packageName, const String& stageName);
+
+ static bool ContainsDotDot(const String& path);
+ static bool ValidatePackageName(const String& packageName);
+
+ static inline
+ bool ValidateStageName(const String& stageName)
+ {
+ return ValidateFreshName(stageName);
+ }
+
+ static std::mutex& GetStaticPackageMutex();
+ static std::mutex& GetStaticActiveStageMutex();
+
+private:
+ static void CollectPaths(const String& path, std::vector<std::pair<String, bool> >& paths);
+
+ static void WritePackageConfig(const String& packageName);
+ static void WriteStageConfig(const String& packageName, const String& stageName);
+
+ static void TryActivateStageCallback(const ProcessResult& pr, const String& packageName, const String& stageName, bool activate,
+ bool reload, const Shared<Defer>::Ptr& resetPackageUpdates);
+
+ static bool ValidateFreshName(const String& name);
+};
+
+}
+
+#endif /* CONFIGMODULEUTILITY_H */
diff --git a/lib/remote/configstageshandler.cpp b/lib/remote/configstageshandler.cpp
new file mode 100644
index 0000000..b5aaadd
--- /dev/null
+++ b/lib/remote/configstageshandler.cpp
@@ -0,0 +1,225 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configstageshandler.hpp"
+#include "remote/configpackageutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/application.hpp"
+#include "base/defer.hpp"
+#include "base/exception.hpp"
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/config/stages", ConfigStagesHandler);
+
+std::atomic<bool> ConfigStagesHandler::m_RunningPackageUpdates (false);
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 5)
+ return false;
+
+ if (request.method() == http::verb::get)
+ HandleGet(user, request, url, response, params);
+ else if (request.method() == http::verb::post)
+ HandlePost(user, request, url, response, params);
+ else if (request.method() == http::verb::delete_)
+ HandleDelete(user, request, url, response, params);
+ else
+ return false;
+
+ return true;
+}
+
+void ConfigStagesHandler::HandleGet(
+ const ApiUser::Ptr& user,
+ 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");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ if (url->GetPath().size() >= 5)
+ params->Set("stage", url->GetPath()[4]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+ String stageName = HttpUtility::GetLastParameter(params, "stage");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+
+ if (!ConfigPackageUtility::ValidateStageName(stageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid stage name '" + stageName + "'.");
+
+ ArrayData results;
+
+ std::vector<std::pair<String, bool> > paths = ConfigPackageUtility::GetFiles(packageName, stageName);
+
+ String prefixPath = ConfigPackageUtility::GetPackageDir() + "/" + packageName + "/" + stageName + "/";
+
+ for (const auto& kv : paths) {
+ results.push_back(new Dictionary({
+ { "type", kv.second ? "directory" : "file" },
+ { "name", kv.first.SubStr(prefixPath.GetLength()) }
+ }));
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
+void ConfigStagesHandler::HandlePost(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/modify");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+
+ bool reload = true;
+
+ if (params->Contains("reload"))
+ reload = HttpUtility::GetLastParameter(params, "reload");
+
+ bool activate = true;
+
+ if (params->Contains("activate"))
+ activate = HttpUtility::GetLastParameter(params, "activate");
+
+ Dictionary::Ptr files = params->Get("files");
+
+ String stageName;
+
+ try {
+ if (!files)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Parameter 'files' must be specified."));
+
+ if (reload && !activate)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Parameter 'reload' must be false when 'activate' is false."));
+
+ if (m_RunningPackageUpdates.exchange(true)) {
+ return HttpUtility::SendJsonError(response, params, 423,
+ "Conflicting request, there is already an ongoing package update in progress. Please try it again later.");
+ }
+
+ auto resetPackageUpdates (Shared<Defer>::Make([]() { ConfigStagesHandler::m_RunningPackageUpdates.store(false); }));
+
+ std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
+
+ stageName = ConfigPackageUtility::CreateStage(packageName, files);
+
+ /* validate the config. on success, activate stage and reload */
+ ConfigPackageUtility::AsyncTryActivateStage(packageName, stageName, activate, reload, resetPackageUpdates);
+ } catch (const std::exception& ex) {
+ return HttpUtility::SendJsonError(response, params, 500,
+ "Stage creation failed.",
+ DiagnosticInformation(ex));
+ }
+
+
+ String responseStatus = "Created stage. ";
+
+ if (reload)
+ responseStatus += "Reload triggered.";
+ else
+ responseStatus += "Reload skipped.";
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "package", packageName },
+ { "stage", stageName },
+ { "code", 200 },
+ { "status", responseStatus }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
+void ConfigStagesHandler::HandleDelete(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+)
+{
+ namespace http = boost::beast::http;
+
+ FilterUtility::CheckPermission(user, "config/modify");
+
+ if (url->GetPath().size() >= 4)
+ params->Set("package", url->GetPath()[3]);
+
+ if (url->GetPath().size() >= 5)
+ params->Set("stage", url->GetPath()[4]);
+
+ String packageName = HttpUtility::GetLastParameter(params, "package");
+ String stageName = HttpUtility::GetLastParameter(params, "stage");
+
+ if (!ConfigPackageUtility::ValidatePackageName(packageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid package name '" + packageName + "'.");
+
+ if (!ConfigPackageUtility::ValidateStageName(stageName))
+ return HttpUtility::SendJsonError(response, params, 400, "Invalid stage name '" + stageName + "'.");
+
+ try {
+ ConfigPackageUtility::DeleteStage(packageName, stageName);
+ } catch (const std::exception& ex) {
+ return HttpUtility::SendJsonError(response, params, 500,
+ "Failed to delete stage '" + stageName + "' in package '" + packageName + "'.",
+ DiagnosticInformation(ex));
+ }
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "code", 200 },
+ { "package", packageName },
+ { "stage", stageName },
+ { "status", "Stage deleted." }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+}
+
diff --git a/lib/remote/configstageshandler.hpp b/lib/remote/configstageshandler.hpp
new file mode 100644
index 0000000..88f248c
--- /dev/null
+++ b/lib/remote/configstageshandler.hpp
@@ -0,0 +1,56 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONFIGSTAGESHANDLER_H
+#define CONFIGSTAGESHANDLER_H
+
+#include "remote/httphandler.hpp"
+#include <atomic>
+
+namespace icinga
+{
+
+class ConfigStagesHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigStagesHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+
+private:
+ void HandleGet(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+ void HandlePost(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+ void HandleDelete(
+ const ApiUser::Ptr& user,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ const Url::Ptr& url,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params
+ );
+
+ static std::atomic<bool> m_RunningPackageUpdates;
+};
+
+}
+
+#endif /* CONFIGSTAGESHANDLER_H */
diff --git a/lib/remote/consolehandler.cpp b/lib/remote/consolehandler.cpp
new file mode 100644
index 0000000..f5a470a
--- /dev/null
+++ b/lib/remote/consolehandler.cpp
@@ -0,0 +1,327 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/configobjectslock.hpp"
+#include "remote/consolehandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "config/configcompiler.hpp"
+#include "base/configtype.hpp"
+#include "base/configwriter.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/timer.hpp"
+#include "base/namespace.hpp"
+#include "base/initialize.hpp"
+#include "base/utility.hpp"
+#include <boost/thread/once.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/console", ConsoleHandler);
+
+static std::mutex l_QueryMutex;
+static std::map<String, ApiScriptFrame> l_ApiScriptFrames;
+static Timer::Ptr l_FrameCleanupTimer;
+static std::mutex l_ApiScriptMutex;
+
+static void ScriptFrameCleanupHandler()
+{
+ std::unique_lock<std::mutex> lock(l_ApiScriptMutex);
+
+ std::vector<String> cleanup_keys;
+
+ typedef std::pair<String, ApiScriptFrame> KVPair;
+
+ for (const KVPair& kv : l_ApiScriptFrames) {
+ if (kv.second.Seen < Utility::GetTime() - 1800)
+ cleanup_keys.push_back(kv.first);
+ }
+
+ for (const String& key : cleanup_keys)
+ l_ApiScriptFrames.erase(key);
+}
+
+static void EnsureFrameCleanupTimer()
+{
+ static boost::once_flag once = BOOST_ONCE_INIT;
+
+ boost::call_once(once, []() {
+ l_FrameCleanupTimer = Timer::Create();
+ l_FrameCleanupTimer->OnTimerExpired.connect([](const Timer * const&) { ScriptFrameCleanupHandler(); });
+ l_FrameCleanupTimer->SetInterval(30);
+ l_FrameCleanupTimer->Start();
+ });
+}
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() != 3)
+ return false;
+
+ if (request.method() != http::verb::post)
+ return false;
+
+ QueryDescription qd;
+
+ String methodName = url->GetPath()[2];
+
+ FilterUtility::CheckPermission(user, "console");
+
+ String session = HttpUtility::GetLastParameter(params, "session");
+
+ if (session.IsEmpty())
+ session = Utility::NewUniqueID();
+
+ String command = HttpUtility::GetLastParameter(params, "command");
+
+ bool sandboxed = HttpUtility::GetLastParameter(params, "sandboxed");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading.");
+ return true;
+ }
+
+ if (methodName == "execute-script")
+ return ExecuteScriptHelper(request, response, params, command, session, sandboxed);
+ else if (methodName == "auto-complete-script")
+ return AutocompleteScriptHelper(request, response, params, command, session, sandboxed);
+
+ HttpUtility::SendJsonError(response, params, 400, "Invalid method specified: " + methodName);
+ return true;
+}
+
+bool ConsoleHandler::ExecuteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
+{
+ namespace http = boost::beast::http;
+
+ Log(LogNotice, "Console")
+ << "Executing expression: " << command;
+
+ EnsureFrameCleanupTimer();
+
+ ApiScriptFrame& lsf = l_ApiScriptFrames[session];
+ lsf.Seen = Utility::GetTime();
+
+ if (!lsf.Locals)
+ lsf.Locals = new Dictionary();
+
+ String fileName = "<" + Convert::ToString(lsf.NextLine) + ">";
+ lsf.NextLine++;
+
+ lsf.Lines[fileName] = command;
+
+ Dictionary::Ptr resultInfo;
+ std::unique_ptr<Expression> expr;
+ Value exprResult;
+
+ try {
+ expr = ConfigCompiler::CompileText(fileName, command);
+
+ ScriptFrame frame(true);
+ frame.Locals = lsf.Locals;
+ frame.Self = lsf.Locals;
+ frame.Sandboxed = sandboxed;
+
+ exprResult = expr->Evaluate(frame);
+
+ resultInfo = new Dictionary({
+ { "code", 200 },
+ { "status", "Executed successfully." },
+ { "result", Serialize(exprResult, 0) }
+ });
+ } catch (const ScriptError& ex) {
+ DebugInfo di = ex.GetDebugInfo();
+
+ std::ostringstream msgbuf;
+
+ msgbuf << di.Path << ": " << lsf.Lines[di.Path] << "\n"
+ << String(di.Path.GetLength() + 2, ' ')
+ << String(di.FirstColumn, ' ') << String(di.LastColumn - di.FirstColumn + 1, '^') << "\n"
+ << ex.what() << "\n";
+
+ resultInfo = new Dictionary({
+ { "code", 500 },
+ { "status", String(msgbuf.str()) },
+ { "incomplete_expression", ex.IsIncompleteExpression() },
+ { "debug_info", new Dictionary({
+ { "path", di.Path },
+ { "first_line", di.FirstLine },
+ { "first_column", di.FirstColumn },
+ { "last_line", di.LastLine },
+ { "last_column", di.LastColumn }
+ }) }
+ });
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ resultInfo }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
+bool ConsoleHandler::AutocompleteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
+{
+ namespace http = boost::beast::http;
+
+ Log(LogInformation, "Console")
+ << "Auto-completing expression: " << command;
+
+ EnsureFrameCleanupTimer();
+
+ ApiScriptFrame& lsf = l_ApiScriptFrames[session];
+ lsf.Seen = Utility::GetTime();
+
+ if (!lsf.Locals)
+ lsf.Locals = new Dictionary();
+
+
+ ScriptFrame frame(true);
+ frame.Locals = lsf.Locals;
+ frame.Self = lsf.Locals;
+ frame.Sandboxed = sandboxed;
+
+ Dictionary::Ptr result1 = new Dictionary({
+ { "code", 200 },
+ { "status", "Auto-completed successfully." },
+ { "suggestions", Array::FromVector(GetAutocompletionSuggestions(command, frame)) }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
+static void AddSuggestion(std::vector<String>& matches, const String& word, const String& suggestion)
+{
+ if (suggestion.Find(word) != 0)
+ return;
+
+ matches.push_back(suggestion);
+}
+
+static void AddSuggestions(std::vector<String>& matches, const String& word, const String& pword, bool withFields, const Value& value)
+{
+ String prefix;
+
+ if (!pword.IsEmpty())
+ prefix = pword + ".";
+
+ if (value.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dict = value;
+
+ ObjectLock olock(dict);
+ for (const Dictionary::Pair& kv : dict) {
+ AddSuggestion(matches, word, prefix + kv.first);
+ }
+ }
+
+ if (value.IsObjectType<Namespace>()) {
+ Namespace::Ptr ns = value;
+
+ ObjectLock olock(ns);
+ for (const Namespace::Pair& kv : ns) {
+ AddSuggestion(matches, word, prefix + kv.first);
+ }
+ }
+
+ if (withFields) {
+ Type::Ptr type = value.GetReflectionType();
+
+ for (int i = 0; i < type->GetFieldCount(); i++) {
+ Field field = type->GetFieldInfo(i);
+
+ AddSuggestion(matches, word, prefix + field.Name);
+ }
+
+ while (type) {
+ Object::Ptr prototype = type->GetPrototype();
+ Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>(prototype);
+
+ if (dict) {
+ ObjectLock olock(dict);
+ for (const Dictionary::Pair& kv : dict) {
+ AddSuggestion(matches, word, prefix + kv.first);
+ }
+ }
+
+ type = type->GetBaseType();
+ }
+ }
+}
+
+std::vector<String> ConsoleHandler::GetAutocompletionSuggestions(const String& word, ScriptFrame& frame)
+{
+ std::vector<String> matches;
+
+ for (const String& keyword : ConfigWriter::GetKeywords()) {
+ AddSuggestion(matches, word, keyword);
+ }
+
+ {
+ ObjectLock olock(frame.Locals);
+ for (const Dictionary::Pair& kv : frame.Locals) {
+ AddSuggestion(matches, word, kv.first);
+ }
+ }
+
+ {
+ ObjectLock olock(ScriptGlobal::GetGlobals());
+ for (const Namespace::Pair& kv : ScriptGlobal::GetGlobals()) {
+ AddSuggestion(matches, word, kv.first);
+ }
+ }
+
+ Namespace::Ptr systemNS = ScriptGlobal::Get("System");
+
+ AddSuggestions(matches, word, "", false, systemNS);
+ AddSuggestions(matches, word, "", true, systemNS->Get("Configuration"));
+ AddSuggestions(matches, word, "", false, ScriptGlobal::Get("Types"));
+ AddSuggestions(matches, word, "", false, ScriptGlobal::Get("Icinga"));
+
+ String::SizeType cperiod = word.RFind(".");
+
+ if (cperiod != String::NPos) {
+ String pword = word.SubStr(0, cperiod);
+
+ Value value;
+
+ try {
+ std::unique_ptr<Expression> expr = ConfigCompiler::CompileText("temp", pword);
+
+ if (expr)
+ value = expr->Evaluate(frame);
+
+ AddSuggestions(matches, word, pword, true, value);
+ } catch (...) { /* Ignore the exception */ }
+ }
+
+ return matches;
+}
diff --git a/lib/remote/consolehandler.hpp b/lib/remote/consolehandler.hpp
new file mode 100644
index 0000000..df0d77d
--- /dev/null
+++ b/lib/remote/consolehandler.hpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONSOLEHANDLER_H
+#define CONSOLEHANDLER_H
+
+#include "remote/httphandler.hpp"
+#include "base/scriptframe.hpp"
+
+namespace icinga
+{
+
+struct ApiScriptFrame
+{
+ double Seen{0};
+ int NextLine{1};
+ std::map<String, String> Lines;
+ Dictionary::Ptr Locals;
+};
+
+class ConsoleHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConsoleHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+
+ static std::vector<String> GetAutocompletionSuggestions(const String& word, ScriptFrame& frame);
+
+private:
+ static bool ExecuteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed);
+ static bool AutocompleteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed);
+
+};
+
+}
+
+#endif /* CONSOLEHANDLER_H */
diff --git a/lib/remote/createobjecthandler.cpp b/lib/remote/createobjecthandler.cpp
new file mode 100644
index 0000000..598eeec
--- /dev/null
+++ b/lib/remote/createobjecthandler.cpp
@@ -0,0 +1,155 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/createobjecthandler.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/configobjectutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/apiaction.hpp"
+#include "remote/zone.hpp"
+#include "base/configtype.hpp"
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/objects", CreateObjectHandler);
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() != 4)
+ return false;
+
+ if (request.method() != http::verb::put)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ FilterUtility::CheckPermission(user, "objects/create/" + type->GetName());
+
+ String name = url->GetPath()[3];
+ Array::Ptr templates = params->Get("templates");
+ Dictionary::Ptr attrs = params->Get("attrs");
+
+ /* Put created objects into the local zone if not explicitly defined.
+ * This allows additional zone members to sync the
+ * configuration at some later point.
+ */
+ Zone::Ptr localZone = Zone::GetLocalZone();
+ String localZoneName;
+
+ if (localZone) {
+ localZoneName = localZone->GetName();
+
+ if (!attrs) {
+ attrs = new Dictionary({
+ { "zone", localZoneName }
+ });
+ } else if (!attrs->Contains("zone")) {
+ attrs->Set("zone", localZoneName);
+ }
+ }
+
+ /* Sanity checks for unique groups array. */
+ if (attrs->Contains("groups")) {
+ Array::Ptr groups = attrs->Get("groups");
+
+ if (groups)
+ attrs->Set("groups", groups->Unique());
+ }
+
+ Dictionary::Ptr result1 = new Dictionary();
+ String status;
+ Array::Ptr errors = new Array();
+ Array::Ptr diagnosticInformation = new Array();
+
+ bool ignoreOnError = false;
+
+ if (params->Contains("ignore_on_error"))
+ ignoreOnError = HttpUtility::GetLastParameter(params, "ignore_on_error");
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ String config;
+
+ bool verbose = false;
+
+ if (params)
+ verbose = HttpUtility::GetLastParameter(params, "verbose");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
+ return true;
+ }
+
+ /* Object creation can cause multiple errors and optionally diagnostic information.
+ * We can't use SendJsonError() here.
+ */
+ try {
+ config = ConfigObjectUtility::CreateObjectConfig(type, name, ignoreOnError, templates, attrs);
+ } catch (const std::exception& ex) {
+ errors->Add(DiagnosticInformation(ex, false));
+ diagnosticInformation->Add(DiagnosticInformation(ex));
+
+ if (verbose)
+ result1->Set("diagnostic_information", diagnosticInformation);
+
+ result1->Set("errors", errors);
+ result1->Set("code", 500);
+ result1->Set("status", "Object could not be created.");
+
+ response.result(http::status::internal_server_error);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+ }
+
+ if (!ConfigObjectUtility::CreateObject(type, name, config, errors, diagnosticInformation)) {
+ result1->Set("errors", errors);
+ result1->Set("code", 500);
+ result1->Set("status", "Object could not be created.");
+
+ if (verbose)
+ result1->Set("diagnostic_information", diagnosticInformation);
+
+ response.result(http::status::internal_server_error);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+ }
+
+ auto *ctype = dynamic_cast<ConfigType *>(type.get());
+ ConfigObject::Ptr obj = ctype->GetObject(name);
+
+ result1->Set("code", 200);
+
+ if (obj)
+ result1->Set("status", "Object was created");
+ else if (!obj && ignoreOnError)
+ result1->Set("status", "Object was not created but 'ignore_on_error' was set to true");
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/createobjecthandler.hpp b/lib/remote/createobjecthandler.hpp
new file mode 100644
index 0000000..4bcf21b
--- /dev/null
+++ b/lib/remote/createobjecthandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CREATEOBJECTHANDLER_H
+#define CREATEOBJECTHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class CreateObjectHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CreateObjectHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* CREATEOBJECTHANDLER_H */
diff --git a/lib/remote/deleteobjecthandler.cpp b/lib/remote/deleteobjecthandler.cpp
new file mode 100644
index 0000000..a4fd98d
--- /dev/null
+++ b/lib/remote/deleteobjecthandler.cpp
@@ -0,0 +1,123 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/deleteobjecthandler.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/configobjectutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/apiaction.hpp"
+#include "config/configitem.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/objects", DeleteObjectHandler);
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() != http::verb::delete_)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ QueryDescription qd;
+ qd.Types.insert(type->GetName());
+ qd.Permission = "objects/delete/" + type->GetName();
+
+ params->Set("type", type->GetName());
+
+ if (url->GetPath().size() >= 4) {
+ String attr = type->GetName();
+ boost::algorithm::to_lower(attr);
+ params->Set(attr, url->GetPath()[3]);
+ }
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ bool cascade = HttpUtility::GetLastParameter(params, "cascade");
+ bool verbose = HttpUtility::GetLastParameter(params, "verbose");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
+ return true;
+ }
+
+ ArrayData results;
+
+ bool success = true;
+
+ for (const ConfigObject::Ptr& obj : objs) {
+ int code;
+ String status;
+ Array::Ptr errors = new Array();
+ Array::Ptr diagnosticInformation = new Array();
+
+ if (!ConfigObjectUtility::DeleteObject(obj, cascade, errors, diagnosticInformation)) {
+ code = 500;
+ status = "Object could not be deleted.";
+ success = false;
+ } else {
+ code = 200;
+ status = "Object was deleted.";
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "type", type->GetName() },
+ { "name", obj->GetName() },
+ { "code", code },
+ { "status", status },
+ { "errors", errors }
+ });
+
+ if (verbose)
+ result->Set("diagnostic_information", diagnosticInformation);
+
+ results.push_back(result);
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ if (!success)
+ response.result(http::status::internal_server_error);
+ else
+ response.result(http::status::ok);
+
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
diff --git a/lib/remote/deleteobjecthandler.hpp b/lib/remote/deleteobjecthandler.hpp
new file mode 100644
index 0000000..19a46e4
--- /dev/null
+++ b/lib/remote/deleteobjecthandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DELETEOBJECTHANDLER_H
+#define DELETEOBJECTHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class DeleteObjectHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(DeleteObjectHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* DELETEOBJECTHANDLER_H */
diff --git a/lib/remote/endpoint.cpp b/lib/remote/endpoint.cpp
new file mode 100644
index 0000000..e534fc1
--- /dev/null
+++ b/lib/remote/endpoint.cpp
@@ -0,0 +1,138 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/endpoint.hpp"
+#include "remote/endpoint-ti.cpp"
+#include "remote/apilistener.hpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/zone.hpp"
+#include "base/configtype.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Endpoint);
+
+boost::signals2::signal<void(const Endpoint::Ptr&, const JsonRpcConnection::Ptr&)> Endpoint::OnConnected;
+boost::signals2::signal<void(const Endpoint::Ptr&, const JsonRpcConnection::Ptr&)> Endpoint::OnDisconnected;
+
+void Endpoint::OnAllConfigLoaded()
+{
+ ObjectImpl<Endpoint>::OnAllConfigLoaded();
+
+ if (!m_Zone)
+ BOOST_THROW_EXCEPTION(ScriptError("Endpoint '" + GetName() +
+ "' does not belong to a zone.", GetDebugInfo()));
+}
+
+void Endpoint::SetCachedZone(const Zone::Ptr& zone)
+{
+ if (m_Zone)
+ BOOST_THROW_EXCEPTION(ScriptError("Endpoint '" + GetName()
+ + "' is in more than one zone.", GetDebugInfo()));
+
+ m_Zone = zone;
+}
+
+void Endpoint::AddClient(const JsonRpcConnection::Ptr& client)
+{
+ bool was_master = ApiListener::GetInstance()->IsMaster();
+
+ {
+ std::unique_lock<std::mutex> lock(m_ClientsLock);
+ m_Clients.insert(client);
+ }
+
+ bool is_master = ApiListener::GetInstance()->IsMaster();
+
+ if (was_master != is_master)
+ ApiListener::OnMasterChanged(is_master);
+
+ OnConnected(this, client);
+}
+
+void Endpoint::RemoveClient(const JsonRpcConnection::Ptr& client)
+{
+ bool was_master = ApiListener::GetInstance()->IsMaster();
+
+ {
+ std::unique_lock<std::mutex> lock(m_ClientsLock);
+ m_Clients.erase(client);
+
+ Log(LogWarning, "ApiListener")
+ << "Removing API client for endpoint '" << GetName() << "'. " << m_Clients.size() << " API clients left.";
+
+ SetConnecting(false);
+ }
+
+ bool is_master = ApiListener::GetInstance()->IsMaster();
+
+ if (was_master != is_master)
+ ApiListener::OnMasterChanged(is_master);
+
+ OnDisconnected(this, client);
+}
+
+std::set<JsonRpcConnection::Ptr> Endpoint::GetClients() const
+{
+ std::unique_lock<std::mutex> lock(m_ClientsLock);
+ return m_Clients;
+}
+
+Zone::Ptr Endpoint::GetZone() const
+{
+ return m_Zone;
+}
+
+bool Endpoint::GetConnected() const
+{
+ std::unique_lock<std::mutex> lock(m_ClientsLock);
+ return !m_Clients.empty();
+}
+
+Endpoint::Ptr Endpoint::GetLocalEndpoint()
+{
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return nullptr;
+
+ return listener->GetLocalEndpoint();
+}
+
+void Endpoint::AddMessageSent(int bytes)
+{
+ double time = Utility::GetTime();
+ m_MessagesSent.InsertValue(time, 1);
+ m_BytesSent.InsertValue(time, bytes);
+ SetLastMessageSent(time);
+}
+
+void Endpoint::AddMessageReceived(int bytes)
+{
+ double time = Utility::GetTime();
+ m_MessagesReceived.InsertValue(time, 1);
+ m_BytesReceived.InsertValue(time, bytes);
+ SetLastMessageReceived(time);
+}
+
+double Endpoint::GetMessagesSentPerSecond() const
+{
+ return m_MessagesSent.CalculateRate(Utility::GetTime(), 60);
+}
+
+double Endpoint::GetMessagesReceivedPerSecond() const
+{
+ return m_MessagesReceived.CalculateRate(Utility::GetTime(), 60);
+}
+
+double Endpoint::GetBytesSentPerSecond() const
+{
+ return m_BytesSent.CalculateRate(Utility::GetTime(), 60);
+}
+
+double Endpoint::GetBytesReceivedPerSecond() const
+{
+ return m_BytesReceived.CalculateRate(Utility::GetTime(), 60);
+}
diff --git a/lib/remote/endpoint.hpp b/lib/remote/endpoint.hpp
new file mode 100644
index 0000000..d641c2c
--- /dev/null
+++ b/lib/remote/endpoint.hpp
@@ -0,0 +1,68 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ENDPOINT_H
+#define ENDPOINT_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/endpoint-ti.hpp"
+#include "base/ringbuffer.hpp"
+#include <set>
+
+namespace icinga
+{
+
+class JsonRpcConnection;
+class Zone;
+
+/**
+ * An endpoint that can be used to send and receive messages.
+ *
+ * @ingroup remote
+ */
+class Endpoint final : public ObjectImpl<Endpoint>
+{
+public:
+ DECLARE_OBJECT(Endpoint);
+ DECLARE_OBJECTNAME(Endpoint);
+
+ static boost::signals2::signal<void(const Endpoint::Ptr&, const intrusive_ptr<JsonRpcConnection>&)> OnConnected;
+ static boost::signals2::signal<void(const Endpoint::Ptr&, const intrusive_ptr<JsonRpcConnection>&)> OnDisconnected;
+
+ void AddClient(const intrusive_ptr<JsonRpcConnection>& client);
+ void RemoveClient(const intrusive_ptr<JsonRpcConnection>& client);
+ std::set<intrusive_ptr<JsonRpcConnection> > GetClients() const;
+
+ intrusive_ptr<Zone> GetZone() const;
+
+ bool GetConnected() const override;
+
+ static Endpoint::Ptr GetLocalEndpoint();
+
+ void SetCachedZone(const intrusive_ptr<Zone>& zone);
+
+ void AddMessageSent(int bytes);
+ void AddMessageReceived(int bytes);
+
+ double GetMessagesSentPerSecond() const override;
+ double GetMessagesReceivedPerSecond() const override;
+
+ double GetBytesSentPerSecond() const override;
+ double GetBytesReceivedPerSecond() const override;
+
+protected:
+ void OnAllConfigLoaded() override;
+
+private:
+ mutable std::mutex m_ClientsLock;
+ std::set<intrusive_ptr<JsonRpcConnection> > m_Clients;
+ intrusive_ptr<Zone> m_Zone;
+
+ mutable RingBuffer m_MessagesSent{60};
+ mutable RingBuffer m_MessagesReceived{60};
+ mutable RingBuffer m_BytesSent{60};
+ mutable RingBuffer m_BytesReceived{60};
+};
+
+}
+
+#endif /* ENDPOINT_H */
diff --git a/lib/remote/endpoint.ti b/lib/remote/endpoint.ti
new file mode 100644
index 0000000..78551ec
--- /dev/null
+++ b/lib/remote/endpoint.ti
@@ -0,0 +1,59 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+#include <cstdint>
+
+library remote;
+
+namespace icinga
+{
+
+class Endpoint : ConfigObject
+{
+ load_after Zone;
+
+ [config] String host;
+ [config, required] String port {
+ default {{{ return "5665"; }}}
+ };
+ [config] double log_duration {
+ default {{{ return 86400; }}}
+ };
+
+ [state] Timestamp local_log_position;
+ [state] Timestamp remote_log_position;
+ [state] "unsigned long" icinga_version {
+ default {{{ return 0; }}}
+ };
+ [state] uint_fast64_t capabilities {
+ default {{{ return 0; }}}
+ };
+
+ [no_user_modify] bool connecting;
+ [no_user_modify] bool syncing;
+
+ [no_user_modify, no_storage] bool connected {
+ get;
+ };
+
+ Timestamp last_message_sent;
+ Timestamp last_message_received;
+
+ [no_user_modify, no_storage] double messages_sent_per_second {
+ get;
+ };
+
+ [no_user_modify, no_storage] double messages_received_per_second {
+ get;
+ };
+
+ [no_user_modify, no_storage] double bytes_sent_per_second {
+ get;
+ };
+
+ [no_user_modify, no_storage] double bytes_received_per_second {
+ get;
+ };
+};
+
+}
diff --git a/lib/remote/eventqueue.cpp b/lib/remote/eventqueue.cpp
new file mode 100644
index 0000000..d79b615
--- /dev/null
+++ b/lib/remote/eventqueue.cpp
@@ -0,0 +1,351 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "config/configcompiler.hpp"
+#include "remote/eventqueue.hpp"
+#include "remote/filterutility.hpp"
+#include "base/io-engine.hpp"
+#include "base/singleton.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include <boost/asio/spawn.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/date_time/posix_time/ptime.hpp>
+#include <boost/system/error_code.hpp>
+#include <chrono>
+#include <utility>
+
+using namespace icinga;
+
+EventQueue::EventQueue(String name)
+ : m_Name(std::move(name))
+{ }
+
+bool EventQueue::CanProcessEvent(const String& type) const
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ return m_Types.find(type) != m_Types.end();
+}
+
+void EventQueue::ProcessEvent(const Dictionary::Ptr& event)
+{
+ Namespace::Ptr frameNS = new Namespace();
+ ScriptFrame frame(true, frameNS);
+ frame.Sandboxed = true;
+
+ try {
+ if (!FilterUtility::EvaluateFilter(frame, m_Filter.get(), event, "event"))
+ return;
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "EventQueue")
+ << "Error occurred while evaluating event filter for queue '" << m_Name << "': " << DiagnosticInformation(ex);
+ return;
+ }
+
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ typedef std::pair<void *const, std::deque<Dictionary::Ptr> > kv_pair;
+ for (kv_pair& kv : m_Events) {
+ kv.second.push_back(event);
+ }
+
+ m_CV.notify_all();
+}
+
+void EventQueue::AddClient(void *client)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ auto result = m_Events.insert(std::make_pair(client, std::deque<Dictionary::Ptr>()));
+ ASSERT(result.second);
+
+#ifndef I2_DEBUG
+ (void)result;
+#endif /* I2_DEBUG */
+}
+
+void EventQueue::RemoveClient(void *client)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ m_Events.erase(client);
+}
+
+void EventQueue::UnregisterIfUnused(const String& name, const EventQueue::Ptr& queue)
+{
+ std::unique_lock<std::mutex> lock(queue->m_Mutex);
+
+ if (queue->m_Events.empty())
+ Unregister(name);
+}
+
+void EventQueue::SetTypes(const std::set<String>& types)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_Types = types;
+}
+
+void EventQueue::SetFilter(std::unique_ptr<Expression> filter)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+ m_Filter.swap(filter);
+}
+
+Dictionary::Ptr EventQueue::WaitForEvent(void *client, double timeout)
+{
+ std::unique_lock<std::mutex> lock(m_Mutex);
+
+ for (;;) {
+ 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;
+ }
+
+ if (m_CV.wait_for(lock, std::chrono::duration<double>(timeout)) == std::cv_status::timeout)
+ return nullptr;
+ }
+}
+
+std::vector<EventQueue::Ptr> EventQueue::GetQueuesForType(const String& type)
+{
+ EventQueueRegistry::ItemMap queues = EventQueueRegistry::GetInstance()->GetItems();
+
+ std::vector<EventQueue::Ptr> availQueues;
+
+ typedef std::pair<String, EventQueue::Ptr> kv_pair;
+ for (const kv_pair& kv : queues) {
+ if (kv.second->CanProcessEvent(type))
+ availQueues.push_back(kv.second);
+ }
+
+ return availQueues;
+}
+
+EventQueue::Ptr EventQueue::GetByName(const String& name)
+{
+ return EventQueueRegistry::GetInstance()->GetItem(name);
+}
+
+void EventQueue::Register(const String& name, const EventQueue::Ptr& function)
+{
+ EventQueueRegistry::GetInstance()->Register(name, function);
+}
+
+void EventQueue::Unregister(const String& name)
+{
+ EventQueueRegistry::GetInstance()->Unregister(name);
+}
+
+EventQueueRegistry *EventQueueRegistry::GetInstance()
+{
+ return Singleton<EventQueueRegistry>::GetInstance();
+}
+
+std::mutex EventsInbox::m_FiltersMutex;
+std::map<String, EventsInbox::Filter> EventsInbox::m_Filters ({{"", EventsInbox::Filter{1, Expression::Ptr()}}});
+
+EventsRouter EventsRouter::m_Instance;
+
+EventsInbox::EventsInbox(String filter, const String& filterSource)
+ : m_Timer(IoEngine::Get().GetIoContext())
+{
+ std::unique_lock<std::mutex> lock (m_FiltersMutex);
+ m_Filter = m_Filters.find(filter);
+
+ if (m_Filter == m_Filters.end()) {
+ lock.unlock();
+
+ auto expr (ConfigCompiler::CompileText(filterSource, filter));
+
+ lock.lock();
+
+ m_Filter = m_Filters.find(filter);
+
+ if (m_Filter == m_Filters.end()) {
+ m_Filter = m_Filters.emplace(std::move(filter), Filter{1, Expression::Ptr(expr.release())}).first;
+ } else {
+ ++m_Filter->second.Refs;
+ }
+ } else {
+ ++m_Filter->second.Refs;
+ }
+}
+
+EventsInbox::~EventsInbox()
+{
+ std::unique_lock<std::mutex> lock (m_FiltersMutex);
+
+ if (!--m_Filter->second.Refs) {
+ m_Filters.erase(m_Filter);
+ }
+}
+
+const Expression::Ptr& EventsInbox::GetFilter()
+{
+ return m_Filter->second.Expr;
+}
+
+void EventsInbox::Push(Dictionary::Ptr event)
+{
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ m_Queue.emplace(std::move(event));
+ m_Timer.expires_at(boost::posix_time::neg_infin);
+}
+
+Dictionary::Ptr EventsInbox::Shift(boost::asio::yield_context yc, double timeout)
+{
+ std::unique_lock<std::mutex> lock (m_Mutex, std::defer_lock);
+
+ m_Timer.expires_at(boost::posix_time::neg_infin);
+
+ {
+ boost::system::error_code ec;
+
+ while (!lock.try_lock()) {
+ m_Timer.async_wait(yc[ec]);
+ }
+ }
+
+ if (m_Queue.empty()) {
+ m_Timer.expires_from_now(boost::posix_time::milliseconds((unsigned long)(timeout * 1000.0)));
+ lock.unlock();
+
+ {
+ boost::system::error_code ec;
+ m_Timer.async_wait(yc[ec]);
+
+ while (!lock.try_lock()) {
+ m_Timer.async_wait(yc[ec]);
+ }
+ }
+
+ if (m_Queue.empty()) {
+ return nullptr;
+ }
+ }
+
+ auto event (std::move(m_Queue.front()));
+ m_Queue.pop();
+ return event;
+}
+
+EventsSubscriber::EventsSubscriber(std::set<EventType> types, String filter, const String& filterSource)
+ : m_Types(std::move(types)), m_Inbox(new EventsInbox(std::move(filter), filterSource))
+{
+ EventsRouter::GetInstance().Subscribe(m_Types, m_Inbox);
+}
+
+EventsSubscriber::~EventsSubscriber()
+{
+ EventsRouter::GetInstance().Unsubscribe(m_Types, m_Inbox);
+}
+
+const EventsInbox::Ptr& EventsSubscriber::GetInbox()
+{
+ return m_Inbox;
+}
+
+EventsFilter::EventsFilter(std::map<Expression::Ptr, std::set<EventsInbox::Ptr>> inboxes)
+ : m_Inboxes(std::move(inboxes))
+{
+}
+
+EventsFilter::operator bool()
+{
+ return !m_Inboxes.empty();
+}
+
+void EventsFilter::Push(Dictionary::Ptr event)
+{
+ for (auto& perFilter : m_Inboxes) {
+ if (perFilter.first) {
+ ScriptFrame frame(true, new Namespace());
+ frame.Sandboxed = true;
+
+ try {
+ if (!FilterUtility::EvaluateFilter(frame, perFilter.first.get(), event, "event")) {
+ continue;
+ }
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "EventQueue")
+ << "Error occurred while evaluating event filter for queue: " << DiagnosticInformation(ex);
+ continue;
+ }
+ }
+
+ for (auto& inbox : perFilter.second) {
+ inbox->Push(event);
+ }
+ }
+}
+
+EventsRouter& EventsRouter::GetInstance()
+{
+ return m_Instance;
+}
+
+void EventsRouter::Subscribe(const std::set<EventType>& types, const EventsInbox::Ptr& inbox)
+{
+ const auto& filter (inbox->GetFilter());
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ for (auto type : types) {
+ auto perType (m_Subscribers.find(type));
+
+ if (perType == m_Subscribers.end()) {
+ perType = m_Subscribers.emplace(type, decltype(perType->second)()).first;
+ }
+
+ auto perFilter (perType->second.find(filter));
+
+ if (perFilter == perType->second.end()) {
+ perFilter = perType->second.emplace(filter, decltype(perFilter->second)()).first;
+ }
+
+ perFilter->second.emplace(inbox);
+ }
+}
+
+void EventsRouter::Unsubscribe(const std::set<EventType>& types, const EventsInbox::Ptr& inbox)
+{
+ const auto& filter (inbox->GetFilter());
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ for (auto type : types) {
+ auto perType (m_Subscribers.find(type));
+
+ if (perType != m_Subscribers.end()) {
+ auto perFilter (perType->second.find(filter));
+
+ if (perFilter != perType->second.end()) {
+ perFilter->second.erase(inbox);
+
+ if (perFilter->second.empty()) {
+ perType->second.erase(perFilter);
+ }
+ }
+
+ if (perType->second.empty()) {
+ m_Subscribers.erase(perType);
+ }
+ }
+ }
+}
+
+EventsFilter EventsRouter::GetInboxes(EventType type)
+{
+ std::unique_lock<std::mutex> lock (m_Mutex);
+
+ auto perType (m_Subscribers.find(type));
+
+ if (perType == m_Subscribers.end()) {
+ return EventsFilter({});
+ }
+
+ return EventsFilter(perType->second);
+}
diff --git a/lib/remote/eventqueue.hpp b/lib/remote/eventqueue.hpp
new file mode 100644
index 0000000..32bd34a
--- /dev/null
+++ b/lib/remote/eventqueue.hpp
@@ -0,0 +1,177 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EVENTQUEUE_H
+#define EVENTQUEUE_H
+
+#include "remote/httphandler.hpp"
+#include "base/object.hpp"
+#include "config/expression.hpp"
+#include <boost/asio/deadline_timer.hpp>
+#include <boost/asio/spawn.hpp>
+#include <condition_variable>
+#include <cstddef>
+#include <cstdint>
+#include <mutex>
+#include <set>
+#include <map>
+#include <deque>
+#include <queue>
+
+namespace icinga
+{
+
+class EventQueue final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EventQueue);
+
+ EventQueue(String name);
+
+ bool CanProcessEvent(const String& type) const;
+ void ProcessEvent(const Dictionary::Ptr& event);
+ void AddClient(void *client);
+ void RemoveClient(void *client);
+
+ void SetTypes(const std::set<String>& types);
+ void SetFilter(std::unique_ptr<Expression> filter);
+
+ Dictionary::Ptr WaitForEvent(void *client, double timeout = 5);
+
+ static std::vector<EventQueue::Ptr> GetQueuesForType(const String& type);
+ static void UnregisterIfUnused(const String& name, const EventQueue::Ptr& queue);
+
+ static EventQueue::Ptr GetByName(const String& name);
+ static void Register(const String& name, const EventQueue::Ptr& function);
+ static void Unregister(const String& name);
+
+private:
+ String m_Name;
+
+ mutable std::mutex m_Mutex;
+ std::condition_variable m_CV;
+
+ std::set<String> m_Types;
+ std::unique_ptr<Expression> m_Filter;
+
+ std::map<void *, std::deque<Dictionary::Ptr> > m_Events;
+};
+
+/**
+ * A registry for API event queues.
+ *
+ * @ingroup base
+ */
+class EventQueueRegistry : public Registry<EventQueueRegistry, EventQueue::Ptr>
+{
+public:
+ static EventQueueRegistry *GetInstance();
+};
+
+enum class EventType : uint_fast8_t
+{
+ AcknowledgementCleared,
+ AcknowledgementSet,
+ CheckResult,
+ CommentAdded,
+ CommentRemoved,
+ DowntimeAdded,
+ DowntimeRemoved,
+ DowntimeStarted,
+ DowntimeTriggered,
+ Flapping,
+ Notification,
+ StateChange,
+ ObjectCreated,
+ ObjectDeleted,
+ ObjectModified
+};
+
+class EventsInbox : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EventsInbox);
+
+ EventsInbox(String filter, const String& filterSource);
+ EventsInbox(const EventsInbox&) = delete;
+ EventsInbox(EventsInbox&&) = delete;
+ EventsInbox& operator=(const EventsInbox&) = delete;
+ EventsInbox& operator=(EventsInbox&&) = delete;
+ ~EventsInbox();
+
+ const Expression::Ptr& GetFilter();
+
+ void Push(Dictionary::Ptr event);
+ Dictionary::Ptr Shift(boost::asio::yield_context yc, double timeout = 5);
+
+private:
+ struct Filter
+ {
+ std::size_t Refs;
+ Expression::Ptr Expr;
+ };
+
+ static std::mutex m_FiltersMutex;
+ static std::map<String, Filter> m_Filters;
+
+ std::mutex m_Mutex;
+ decltype(m_Filters.begin()) m_Filter;
+ std::queue<Dictionary::Ptr> m_Queue;
+ boost::asio::deadline_timer m_Timer;
+};
+
+class EventsSubscriber
+{
+public:
+ EventsSubscriber(std::set<EventType> types, String filter, const String& filterSource);
+ EventsSubscriber(const EventsSubscriber&) = delete;
+ EventsSubscriber(EventsSubscriber&&) = delete;
+ EventsSubscriber& operator=(const EventsSubscriber&) = delete;
+ EventsSubscriber& operator=(EventsSubscriber&&) = delete;
+ ~EventsSubscriber();
+
+ const EventsInbox::Ptr& GetInbox();
+
+private:
+ std::set<EventType> m_Types;
+ EventsInbox::Ptr m_Inbox;
+};
+
+class EventsFilter
+{
+public:
+ EventsFilter(std::map<Expression::Ptr, std::set<EventsInbox::Ptr>> inboxes);
+
+ operator bool();
+
+ void Push(Dictionary::Ptr event);
+
+private:
+ std::map<Expression::Ptr, std::set<EventsInbox::Ptr>> m_Inboxes;
+};
+
+class EventsRouter
+{
+public:
+ static EventsRouter& GetInstance();
+
+ void Subscribe(const std::set<EventType>& types, const EventsInbox::Ptr& inbox);
+ void Unsubscribe(const std::set<EventType>& types, const EventsInbox::Ptr& inbox);
+ EventsFilter GetInboxes(EventType type);
+
+private:
+ static EventsRouter m_Instance;
+
+ EventsRouter() = default;
+ EventsRouter(const EventsRouter&) = delete;
+ EventsRouter(EventsRouter&&) = delete;
+ EventsRouter& operator=(const EventsRouter&) = delete;
+ EventsRouter& operator=(EventsRouter&&) = delete;
+ ~EventsRouter() = default;
+
+ std::mutex m_Mutex;
+ std::map<EventType, std::map<Expression::Ptr, std::set<EventsInbox::Ptr>>> m_Subscribers;
+};
+
+}
+
+#endif /* EVENTQUEUE_H */
diff --git a/lib/remote/eventshandler.cpp b/lib/remote/eventshandler.cpp
new file mode 100644
index 0000000..e05ef22
--- /dev/null
+++ b/lib/remote/eventshandler.cpp
@@ -0,0 +1,137 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/eventshandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "config/configcompiler.hpp"
+#include "config/expression.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/objectlock.hpp"
+#include "base/json.hpp"
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/write.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <map>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/events", EventsHandler);
+
+const std::map<String, EventType> l_EventTypes ({
+ {"AcknowledgementCleared", EventType::AcknowledgementCleared},
+ {"AcknowledgementSet", EventType::AcknowledgementSet},
+ {"CheckResult", EventType::CheckResult},
+ {"CommentAdded", EventType::CommentAdded},
+ {"CommentRemoved", EventType::CommentRemoved},
+ {"DowntimeAdded", EventType::DowntimeAdded},
+ {"DowntimeRemoved", EventType::DowntimeRemoved},
+ {"DowntimeStarted", EventType::DowntimeStarted},
+ {"DowntimeTriggered", EventType::DowntimeTriggered},
+ {"Flapping", EventType::Flapping},
+ {"Notification", EventType::Notification},
+ {"StateChange", EventType::StateChange},
+ {"ObjectCreated", EventType::ObjectCreated},
+ {"ObjectDeleted", EventType::ObjectDeleted},
+ {"ObjectModified", EventType::ObjectModified}
+});
+
+const String l_ApiQuery ("<API query>");
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace asio = boost::asio;
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() != 2)
+ return false;
+
+ if (request.method() != http::verb::post)
+ return false;
+
+ if (request.version() == 10) {
+ HttpUtility::SendJsonError(response, params, 400, "HTTP/1.0 not supported for event streams.");
+ return true;
+ }
+
+ Array::Ptr types = params->Get("types");
+
+ if (!types) {
+ HttpUtility::SendJsonError(response, params, 400, "'types' query parameter is required.");
+ return true;
+ }
+
+ {
+ ObjectLock olock(types);
+ for (const String& type : types) {
+ FilterUtility::CheckPermission(user, "events/" + type);
+ }
+ }
+
+ String queueName = HttpUtility::GetLastParameter(params, "queue");
+
+ if (queueName.IsEmpty()) {
+ HttpUtility::SendJsonError(response, params, 400, "'queue' query parameter is required.");
+ return true;
+ }
+
+ std::set<EventType> eventTypes;
+
+ {
+ ObjectLock olock(types);
+ for (const String& type : types) {
+ auto typeId (l_EventTypes.find(type));
+
+ if (typeId != l_EventTypes.end()) {
+ eventTypes.emplace(typeId->second);
+ }
+ }
+ }
+
+ EventsSubscriber subscriber (std::move(eventTypes), HttpUtility::GetLastParameter(params, "filter"), l_ApiQuery);
+
+ server.StartStreaming();
+
+ response.result(http::status::ok);
+ response.set(http::field::content_type, "application/json");
+
+ IoBoundWorkSlot dontLockTheIoThread (yc);
+
+ http::async_write(stream, response, yc);
+ stream.async_flush(yc);
+
+ asio::const_buffer newLine ("\n", 1);
+
+ for (;;) {
+ auto event (subscriber.GetInbox()->Shift(yc));
+
+ if (event) {
+ CpuBoundWork buildingResponse (yc);
+
+ String body = JsonEncode(event);
+
+ boost::algorithm::replace_all(body, "\n", "");
+
+ asio::const_buffer payload (body.CStr(), body.GetLength());
+
+ buildingResponse.Done();
+
+ asio::async_write(stream, payload, yc);
+ asio::async_write(stream, newLine, yc);
+ stream.async_flush(yc);
+ } else if (server.Disconnected()) {
+ return true;
+ }
+ }
+}
+
diff --git a/lib/remote/eventshandler.hpp b/lib/remote/eventshandler.hpp
new file mode 100644
index 0000000..c823415
--- /dev/null
+++ b/lib/remote/eventshandler.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EVENTSHANDLER_H
+#define EVENTSHANDLER_H
+
+#include "remote/httphandler.hpp"
+#include "remote/eventqueue.hpp"
+
+namespace icinga
+{
+
+class EventsHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(EventsHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* EVENTSHANDLER_H */
diff --git a/lib/remote/filterutility.cpp b/lib/remote/filterutility.cpp
new file mode 100644
index 0000000..468b91e
--- /dev/null
+++ b/lib/remote/filterutility.cpp
@@ -0,0 +1,354 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/filterutility.hpp"
+#include "remote/httputility.hpp"
+#include "config/applyrule.hpp"
+#include "config/configcompiler.hpp"
+#include "config/expression.hpp"
+#include "base/namespace.hpp"
+#include "base/json.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <memory>
+
+using namespace icinga;
+
+Type::Ptr FilterUtility::TypeFromPluralName(const String& pluralName)
+{
+ String uname = pluralName;
+ boost::algorithm::to_lower(uname);
+
+ for (const Type::Ptr& type : Type::GetAllTypes()) {
+ String pname = type->GetPluralName();
+ boost::algorithm::to_lower(pname);
+
+ if (uname == pname)
+ return type;
+ }
+
+ return nullptr;
+}
+
+void ConfigObjectTargetProvider::FindTargets(const String& type, const std::function<void (const Value&)>& addTarget) const
+{
+ Type::Ptr ptype = Type::GetByName(type);
+ auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
+
+ if (ctype) {
+ for (const ConfigObject::Ptr& object : ctype->GetObjects()) {
+ addTarget(object);
+ }
+ }
+}
+
+Value ConfigObjectTargetProvider::GetTargetByName(const String& type, const String& name) const
+{
+ ConfigObject::Ptr obj = ConfigObject::GetObject(type, name);
+
+ if (!obj)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Object does not exist."));
+
+ return obj;
+}
+
+bool ConfigObjectTargetProvider::IsValidType(const String& type) const
+{
+ Type::Ptr ptype = Type::GetByName(type);
+
+ if (!ptype)
+ return false;
+
+ return ConfigObject::TypeInstance->IsAssignableFrom(ptype);
+}
+
+String ConfigObjectTargetProvider::GetPluralName(const String& type) const
+{
+ return Type::GetByName(type)->GetPluralName();
+}
+
+bool FilterUtility::EvaluateFilter(ScriptFrame& frame, Expression *filter,
+ const Object::Ptr& target, const String& variableName)
+{
+ if (!filter)
+ return true;
+
+ Type::Ptr type = target->GetReflectionType();
+ String varName;
+
+ if (variableName.IsEmpty())
+ varName = type->GetName().ToLower();
+ else
+ varName = variableName;
+
+ Namespace::Ptr frameNS;
+
+ if (frame.Self.IsEmpty()) {
+ frameNS = new Namespace();
+ frame.Self = frameNS;
+ } else {
+ /* Enforce a namespace object for 'frame.self'. */
+ ASSERT(frame.Self.IsObjectType<Namespace>());
+
+ frameNS = frame.Self;
+
+ ASSERT(frameNS != ScriptGlobal::GetGlobals());
+ }
+
+ frameNS->Set("obj", target);
+ frameNS->Set(varName, target);
+
+ for (int fid = 0; fid < type->GetFieldCount(); fid++) {
+ Field field = type->GetFieldInfo(fid);
+
+ if ((field.Attributes & FANavigation) == 0)
+ continue;
+
+ Object::Ptr joinedObj = target->NavigateField(fid);
+
+ if (field.NavigationName)
+ frameNS->Set(field.NavigationName, joinedObj);
+ else
+ frameNS->Set(field.Name, joinedObj);
+ }
+
+ return Convert::ToBool(filter->Evaluate(frame));
+}
+
+static void FilteredAddTarget(ScriptFrame& permissionFrame, Expression *permissionFilter,
+ ScriptFrame& frame, Expression *ufilter, std::vector<Value>& result, const String& variableName, const Object::Ptr& target)
+{
+ if (FilterUtility::EvaluateFilter(permissionFrame, permissionFilter, target, variableName)) {
+ if (FilterUtility::EvaluateFilter(frame, ufilter, target, variableName)) {
+ result.emplace_back(std::move(target));
+ }
+ }
+}
+
+/**
+ * Checks whether the given API user is granted the given permission
+ *
+ * When you desire an exception to be raised when the given user doesn't have the given permission,
+ * you need to use FilterUtility::CheckPermission().
+ *
+ * @param user ApiUser pointer to the user object you want to check the permission of
+ * @param permission The actual permission you want to check the user permission against
+ * @param permissionFilter Expression pointer that is used as an output buffer for all the filter expressions of the
+ * individual permissions of the given user to be evaluated. It's up to the caller to delete
+ * this pointer when it's not needed any more.
+ *
+ * @return bool
+ */
+bool FilterUtility::HasPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* permissionFilter)
+{
+ if (permissionFilter)
+ *permissionFilter = nullptr;
+
+ if (permission.IsEmpty())
+ return true;
+
+ bool foundPermission = false;
+ String requiredPermission = permission.ToLower();
+
+ Array::Ptr permissions = user->GetPermissions();
+ if (permissions) {
+ ObjectLock olock(permissions);
+ for (const Value& item : permissions) {
+ String permission;
+ Function::Ptr filter;
+ if (item.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dict = item;
+ permission = dict->Get("permission");
+ filter = dict->Get("filter");
+ } else
+ permission = item;
+
+ permission = permission.ToLower();
+
+ if (!Utility::Match(permission, requiredPermission))
+ continue;
+
+ foundPermission = true;
+
+ if (filter && permissionFilter) {
+ std::vector<std::unique_ptr<Expression> > args;
+ args.emplace_back(new GetScopeExpression(ScopeThis));
+ std::unique_ptr<Expression> indexer{new IndexerExpression(std::unique_ptr<Expression>(MakeLiteral(filter)), std::unique_ptr<Expression>(MakeLiteral("call")))};
+ FunctionCallExpression *fexpr = new FunctionCallExpression(std::move(indexer), std::move(args));
+
+ if (!*permissionFilter)
+ permissionFilter->reset(fexpr);
+ else
+ *permissionFilter = std::make_unique<LogicalOrExpression>(std::move(*permissionFilter), std::unique_ptr<Expression>(fexpr));
+ }
+ }
+ }
+
+ if (!foundPermission) {
+ Log(LogWarning, "FilterUtility")
+ << "Missing permission: " << requiredPermission;
+ }
+
+ return foundPermission;
+}
+
+void FilterUtility::CheckPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* permissionFilter)
+{
+ if (!HasPermission(user, permission, permissionFilter)) {
+ BOOST_THROW_EXCEPTION(ScriptError("Missing permission: " + permission.ToLower()));
+ }
+}
+
+std::vector<Value> FilterUtility::GetFilterTargets(const QueryDescription& qd, const Dictionary::Ptr& query, const ApiUser::Ptr& user, const String& variableName)
+{
+ std::vector<Value> result;
+
+ TargetProvider::Ptr provider;
+
+ if (qd.Provider)
+ provider = qd.Provider;
+ else
+ provider = new ConfigObjectTargetProvider();
+
+ std::unique_ptr<Expression> permissionFilter;
+ CheckPermission(user, qd.Permission, &permissionFilter);
+
+ Namespace::Ptr permissionFrameNS = new Namespace();
+ ScriptFrame permissionFrame(false, permissionFrameNS);
+
+ for (const String& type : qd.Types) {
+ String attr = type;
+ boost::algorithm::to_lower(attr);
+
+ if (attr == "type")
+ attr = "name";
+
+ if (query && query->Contains(attr)) {
+ String name = HttpUtility::GetLastParameter(query, attr);
+ Object::Ptr target = provider->GetTargetByName(type, name);
+
+ if (!FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), target, variableName))
+ BOOST_THROW_EXCEPTION(ScriptError("Access denied to object '" + name + "' of type '" + type + "'"));
+
+ result.emplace_back(std::move(target));
+ }
+
+ attr = provider->GetPluralName(type);
+ boost::algorithm::to_lower(attr);
+
+ if (query && query->Contains(attr)) {
+ Array::Ptr names = query->Get(attr);
+ if (names) {
+ ObjectLock olock(names);
+ for (const String& name : names) {
+ Object::Ptr target = provider->GetTargetByName(type, name);
+
+ if (!FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), target, variableName))
+ BOOST_THROW_EXCEPTION(ScriptError("Access denied to object '" + name + "' of type '" + type + "'"));
+
+ result.emplace_back(std::move(target));
+ }
+ }
+ }
+ }
+
+ if ((query && query->Contains("filter")) || result.empty()) {
+ if (!query->Contains("type"))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Type must be specified when using a filter."));
+
+ String type = HttpUtility::GetLastParameter(query, "type");
+
+ if (!provider->IsValidType(type))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid type specified."));
+
+ if (qd.Types.find(type) == qd.Types.end())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid type specified for this query."));
+
+ Namespace::Ptr frameNS = new Namespace();
+ ScriptFrame frame(false, frameNS);
+ frame.Sandboxed = true;
+
+ if (query->Contains("filter")) {
+ String filter = HttpUtility::GetLastParameter(query, "filter");
+ std::unique_ptr<Expression> ufilter = ConfigCompiler::CompileText("<API query>", filter);
+ Dictionary::Ptr filter_vars = query->Get("filter_vars");
+ bool targeted = false;
+ std::vector<ConfigObject::Ptr> targets;
+
+ if (dynamic_cast<ConfigObjectTargetProvider*>(provider.get())) {
+ auto dict (dynamic_cast<DictExpression*>(ufilter.get()));
+
+ if (dict) {
+ auto& subex (dict->GetExpressions());
+
+ if (subex.size() == 1u) {
+ if (type == "Host") {
+ std::vector<const String *> targetNames;
+
+ if (ApplyRule::GetTargetHosts(subex.at(0).get(), targetNames, filter_vars)) {
+ static const auto typeHost (Type::GetByName("Host"));
+ static const auto ctypeHost (dynamic_cast<ConfigType*>(typeHost.get()));
+ targeted = true;
+
+ for (auto name : targetNames) {
+ auto target (ctypeHost->GetObject(*name));
+
+ if (target) {
+ targets.emplace_back(target);
+ }
+ }
+ }
+ } else if (type == "Service") {
+ std::vector<std::pair<const String *, const String *>> targetNames;
+
+ if (ApplyRule::GetTargetServices(subex.at(0).get(), targetNames, filter_vars)) {
+ static const auto typeService (Type::GetByName("Service"));
+ static const auto ctypeService (dynamic_cast<ConfigType*>(typeService.get()));
+ targeted = true;
+
+ for (auto name : targetNames) {
+ auto target (ctypeService->GetObject(*name.first + "!" + *name.second));
+
+ if (target) {
+ targets.emplace_back(target);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (targeted) {
+ for (auto& target : targets) {
+ if (FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), target, variableName)) {
+ result.emplace_back(std::move(target));
+ }
+ }
+ } else {
+ if (filter_vars) {
+ ObjectLock olock (filter_vars);
+
+ for (auto& kv : filter_vars) {
+ frameNS->Set(kv.first, kv.second);
+ }
+ }
+
+ provider->FindTargets(type, [&permissionFrame, &permissionFilter, &frame, &ufilter, &result, variableName](const Object::Ptr& target) {
+ FilteredAddTarget(permissionFrame, permissionFilter.get(), frame, &*ufilter, result, variableName, target);
+ });
+ }
+ } else {
+ /* Ensure to pass a nullptr as filter expression.
+ * GCC 8.1.1 on F28 causes problems, see GH #6533.
+ */
+ provider->FindTargets(type, [&permissionFrame, &permissionFilter, &frame, &result, variableName](const Object::Ptr& target) {
+ FilteredAddTarget(permissionFrame, permissionFilter.get(), frame, nullptr, result, variableName, target);
+ });
+ }
+ }
+
+ return result;
+}
+
diff --git a/lib/remote/filterutility.hpp b/lib/remote/filterutility.hpp
new file mode 100644
index 0000000..7271367
--- /dev/null
+++ b/lib/remote/filterutility.hpp
@@ -0,0 +1,64 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FILTERUTILITY_H
+#define FILTERUTILITY_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/apiuser.hpp"
+#include "config/expression.hpp"
+#include "base/dictionary.hpp"
+#include "base/configobject.hpp"
+#include <set>
+
+namespace icinga
+{
+
+class TargetProvider : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TargetProvider);
+
+ virtual void FindTargets(const String& type, const std::function<void (const Value&)>& addTarget) const = 0;
+ virtual Value GetTargetByName(const String& type, const String& name) const = 0;
+ virtual bool IsValidType(const String& type) const = 0;
+ virtual String GetPluralName(const String& type) const = 0;
+};
+
+class ConfigObjectTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConfigObjectTargetProvider);
+
+ void FindTargets(const String& type, const std::function<void (const Value&)>& addTarget) const override;
+ Value GetTargetByName(const String& type, const String& name) const override;
+ bool IsValidType(const String& type) const override;
+ String GetPluralName(const String& type) const override;
+};
+
+struct QueryDescription
+{
+ std::set<String> Types;
+ TargetProvider::Ptr Provider;
+ String Permission;
+};
+
+/**
+ * Filter utilities.
+ *
+ * @ingroup remote
+ */
+class FilterUtility
+{
+public:
+ static Type::Ptr TypeFromPluralName(const String& pluralName);
+ static void CheckPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* filter = nullptr);
+ static bool HasPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* permissionFilter = nullptr);
+ static std::vector<Value> GetFilterTargets(const QueryDescription& qd, const Dictionary::Ptr& query,
+ const ApiUser::Ptr& user, const String& variableName = String());
+ static bool EvaluateFilter(ScriptFrame& frame, Expression *filter,
+ const Object::Ptr& target, const String& variableName = String());
+};
+
+}
+
+#endif /* FILTERUTILITY_H */
diff --git a/lib/remote/httphandler.cpp b/lib/remote/httphandler.cpp
new file mode 100644
index 0000000..afe510f
--- /dev/null
+++ b/lib/remote/httphandler.cpp
@@ -0,0 +1,129 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/logger.hpp"
+#include "remote/httphandler.hpp"
+#include "remote/httputility.hpp"
+#include "base/singleton.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/beast/http.hpp>
+
+using namespace icinga;
+
+Dictionary::Ptr HttpHandler::m_UrlTree;
+
+void HttpHandler::Register(const Url::Ptr& url, const HttpHandler::Ptr& handler)
+{
+ if (!m_UrlTree)
+ m_UrlTree = new Dictionary();
+
+ Dictionary::Ptr node = m_UrlTree;
+
+ for (const String& elem : url->GetPath()) {
+ Dictionary::Ptr children = node->Get("children");
+
+ if (!children) {
+ children = new Dictionary();
+ node->Set("children", children);
+ }
+
+ Dictionary::Ptr sub_node = children->Get(elem);
+ if (!sub_node) {
+ sub_node = new Dictionary();
+ children->Set(elem, sub_node);
+ }
+
+ node = sub_node;
+ }
+
+ Array::Ptr handlers = node->Get("handlers");
+
+ if (!handlers) {
+ handlers = new Array();
+ node->Set("handlers", handlers);
+ }
+
+ handlers->Add(handler);
+}
+
+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,
+ HttpServerConnection& server
+)
+{
+ Dictionary::Ptr node = m_UrlTree;
+ std::vector<HttpHandler::Ptr> handlers;
+
+ Url::Ptr url = new Url(std::string(request.target()));
+ auto& path (url->GetPath());
+
+ for (std::vector<String>::size_type i = 0; i <= path.size(); i++) {
+ Array::Ptr current_handlers = node->Get("handlers");
+
+ if (current_handlers) {
+ ObjectLock olock(current_handlers);
+ for (const HttpHandler::Ptr& current_handler : current_handlers) {
+ handlers.push_back(current_handler);
+ }
+ }
+
+ Dictionary::Ptr children = node->Get("children");
+
+ if (!children) {
+ node.reset();
+ break;
+ }
+
+ if (i == path.size())
+ break;
+
+ node = children->Get(path[i]);
+
+ if (!node)
+ break;
+ }
+
+ std::reverse(handlers.begin(), handlers.end());
+
+ Dictionary::Ptr params;
+
+ try {
+ params = HttpUtility::FetchRequestParameters(url, request.body());
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid request body: " + DiagnosticInformation(ex, false));
+ return;
+ }
+
+ bool processed = false;
+
+ /*
+ * HandleRequest may throw a permission exception.
+ * DO NOT return a specific permission error. This
+ * allows attackers to guess from words which objects
+ * do exist.
+ */
+ try {
+ for (const HttpHandler::Ptr& handler : handlers) {
+ if (handler->HandleRequest(stream, user, request, url, response, params, yc, server)) {
+ processed = true;
+ break;
+ }
+ }
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "HttpServerConnection")
+ << "Error while processing HTTP request: " << ex.what();
+
+ processed = false;
+ }
+
+ if (!processed) {
+ HttpUtility::SendJsonError(response, params, 404, "The requested path '" + boost::algorithm::join(path, "/") +
+ "' could not be found or the request method is not valid for this path.");
+ return;
+ }
+}
+
diff --git a/lib/remote/httphandler.hpp b/lib/remote/httphandler.hpp
new file mode 100644
index 0000000..a6a7302
--- /dev/null
+++ b/lib/remote/httphandler.hpp
@@ -0,0 +1,74 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HTTPHANDLER_H
+#define HTTPHANDLER_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/url.hpp"
+#include "remote/httpserverconnection.hpp"
+#include "remote/apiuser.hpp"
+#include "base/registry.hpp"
+#include "base/tlsstream.hpp"
+#include <vector>
+#include <boost/asio/spawn.hpp>
+#include <boost/beast/http.hpp>
+
+namespace icinga
+{
+
+/**
+ * HTTP handler.
+ *
+ * @ingroup remote
+ */
+class HttpHandler : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(HttpHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) = 0;
+
+ static void Register(const Url::Ptr& url, const HttpHandler::Ptr& handler);
+ 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,
+ HttpServerConnection& server
+ );
+
+private:
+ static Dictionary::Ptr m_UrlTree;
+};
+
+/**
+ * Helper class for registering HTTP handlers.
+ *
+ * @ingroup remote
+ */
+class RegisterHttpHandler
+{
+public:
+ RegisterHttpHandler(const String& url, const HttpHandler& function);
+};
+
+#define REGISTER_URLHANDLER(url, klass) \
+ INITIALIZE_ONCE([]() { \
+ Url::Ptr uurl = new Url(url); \
+ HttpHandler::Ptr handler = new klass(); \
+ HttpHandler::Register(uurl, handler); \
+ })
+
+}
+
+#endif /* HTTPHANDLER_H */
diff --git a/lib/remote/httpserverconnection.cpp b/lib/remote/httpserverconnection.cpp
new file mode 100644
index 0000000..76cfd3c
--- /dev/null
+++ b/lib/remote/httpserverconnection.cpp
@@ -0,0 +1,613 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/httpserverconnection.hpp"
+#include "remote/httphandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/jsonrpc.hpp"
+#include "base/application.hpp"
+#include "base/base64.hpp"
+#include "base/convert.hpp"
+#include "base/configtype.hpp"
+#include "base/defer.hpp"
+#include "base/exception.hpp"
+#include "base/io-engine.hpp"
+#include "base/logger.hpp"
+#include "base/objectlock.hpp"
+#include "base/timer.hpp"
+#include "base/tlsstream.hpp"
+#include "base/utility.hpp"
+#include <chrono>
+#include <limits>
+#include <memory>
+#include <stdexcept>
+#include <boost/asio/error.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/system/system_error.hpp>
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+auto const l_ServerHeader ("Icinga/" + Application::GetAppVersion());
+
+HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream)
+ : HttpServerConnection(identity, authenticated, stream, IoEngine::Get().GetIoContext())
+{
+}
+
+HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, boost::asio::io_context& io)
+ : m_Stream(stream), m_Seen(Utility::GetTime()), m_IoStrand(io), m_ShuttingDown(false), m_HasStartedStreaming(false),
+ m_CheckLivenessTimer(io)
+{
+ if (authenticated) {
+ m_ApiUser = ApiUser::GetByClientCN(identity);
+ }
+
+ {
+ std::ostringstream address;
+ auto endpoint (stream->lowest_layer().remote_endpoint());
+
+ address << '[' << endpoint.address() << "]:" << endpoint.port();
+
+ m_PeerAddress = address.str();
+ }
+}
+
+void HttpServerConnection::Start()
+{
+ namespace asio = boost::asio;
+
+ HttpServerConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { ProcessMessages(yc); });
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { CheckLiveness(yc); });
+}
+
+void HttpServerConnection::Disconnect()
+{
+ namespace asio = boost::asio;
+
+ HttpServerConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
+ if (!m_ShuttingDown) {
+ m_ShuttingDown = true;
+
+ Log(LogInformation, "HttpServerConnection")
+ << "HTTP client disconnected (from " << m_PeerAddress << ")";
+
+ /*
+ * Do not swallow exceptions in a coroutine.
+ * https://github.com/Icinga/icinga2/issues/7351
+ * We must not catch `detail::forced_unwind exception` as
+ * this is used for unwinding the stack.
+ *
+ * Just use the error_code dummy here.
+ */
+ boost::system::error_code ec;
+
+ m_CheckLivenessTimer.cancel();
+
+ m_Stream->lowest_layer().cancel(ec);
+
+ m_Stream->next_layer().async_shutdown(yc[ec]);
+
+ m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both, ec);
+
+ auto listener (ApiListener::GetInstance());
+
+ if (listener) {
+ CpuBoundWork removeHttpClient (yc);
+
+ listener->RemoveHttpClient(this);
+ }
+ }
+ });
+}
+
+void HttpServerConnection::StartStreaming()
+{
+ namespace asio = boost::asio;
+
+ m_HasStartedStreaming = true;
+
+ HttpServerConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
+ if (!m_ShuttingDown) {
+ char buf[128];
+ asio::mutable_buffer readBuf (buf, 128);
+ boost::system::error_code ec;
+
+ do {
+ m_Stream->async_read_some(readBuf, yc[ec]);
+ } while (!ec);
+
+ Disconnect();
+ }
+ });
+}
+
+bool HttpServerConnection::Disconnected()
+{
+ return m_ShuttingDown;
+}
+
+static inline
+bool EnsureValidHeaders(
+ AsioTlsStream& stream,
+ boost::beast::flat_buffer& buf,
+ boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ bool& shuttingDown,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ if (shuttingDown)
+ return false;
+
+ bool httpError = false;
+ String errorMsg;
+
+ boost::system::error_code ec;
+
+ http::async_read_header(stream, buf, parser, yc[ec]);
+
+ if (ec) {
+ if (ec == boost::asio::error::operation_aborted)
+ return false;
+
+ errorMsg = ec.message();
+ httpError = true;
+ } else {
+ switch (parser.get().version()) {
+ case 10:
+ case 11:
+ break;
+ default:
+ errorMsg = "Unsupported HTTP version";
+ }
+ }
+
+ if (!errorMsg.IsEmpty() || httpError) {
+ response.result(http::status::bad_request);
+
+ if (!httpError && parser.get()[http::field::accept] == "application/json") {
+ HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
+ { "error", 400 },
+ { "status", String("Bad Request: ") + errorMsg }
+ }));
+ } else {
+ response.set(http::field::content_type, "text/html");
+ response.body() = String("<h1>Bad Request</h1><p><pre>") + errorMsg + "</pre></p>";
+ response.content_length(response.body().size());
+ }
+
+ response.set(http::field::connection, "close");
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+
+ return true;
+}
+
+static inline
+void HandleExpect100(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ if (request[http::field::expect] == "100-continue") {
+ http::response<http::string_body> response;
+
+ response.result(http::status::continue_);
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+ }
+}
+
+static inline
+bool HandleAccessControl(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ auto listener (ApiListener::GetInstance());
+
+ if (listener) {
+ auto headerAllowOrigin (listener->GetAccessControlAllowOrigin());
+
+ if (headerAllowOrigin) {
+ CpuBoundWork allowOriginHeader (yc);
+
+ auto allowedOrigins (headerAllowOrigin->ToSet<String>());
+
+ if (!allowedOrigins.empty()) {
+ auto& origin (request[http::field::origin]);
+
+ if (allowedOrigins.find(std::string(origin)) != allowedOrigins.end()) {
+ response.set(http::field::access_control_allow_origin, origin);
+ }
+
+ allowOriginHeader.Done();
+
+ response.set(http::field::access_control_allow_credentials, "true");
+
+ if (request.method() == http::verb::options && !request[http::field::access_control_request_method].empty()) {
+ response.result(http::status::ok);
+ response.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE");
+ response.set(http::field::access_control_allow_headers, "Authorization, Content-Type, X-HTTP-Method-Override");
+ response.body() = "Preflight OK";
+ response.content_length(response.body().size());
+ response.set(http::field::connection, "close");
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+static inline
+bool EnsureAcceptHeader(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ if (request.method() != http::verb::get && request[http::field::accept] != "application/json") {
+ response.result(http::status::bad_request);
+ response.set(http::field::content_type, "text/html");
+ response.body() = "<h1>Accept header is missing or not set to 'application/json'.</h1>";
+ response.content_length(response.body().size());
+ response.set(http::field::connection, "close");
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+
+ return true;
+}
+
+static inline
+bool EnsureAuthenticatedUser(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ ApiUser::Ptr& authenticatedUser,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ if (!authenticatedUser) {
+ Log(LogWarning, "HttpServerConnection")
+ << "Unauthorized request: " << request.method_string() << ' ' << request.target();
+
+ response.result(http::status::unauthorized);
+ response.set(http::field::www_authenticate, "Basic realm=\"Icinga 2\"");
+ response.set(http::field::connection, "close");
+
+ if (request[http::field::accept] == "application/json") {
+ HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
+ { "error", 401 },
+ { "status", "Unauthorized. Please check your user credentials." }
+ }));
+ } else {
+ response.set(http::field::content_type, "text/html");
+ response.body() = "<h1>Unauthorized. Please check your user credentials.</h1>";
+ response.content_length(response.body().size());
+ }
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+
+ return true;
+}
+
+static inline
+bool EnsureValidBody(
+ AsioTlsStream& stream,
+ boost::beast::flat_buffer& buf,
+ boost::beast::http::parser<true, boost::beast::http::string_body>& parser,
+ ApiUser::Ptr& authenticatedUser,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ bool& shuttingDown,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ {
+ size_t maxSize = 1024 * 1024;
+ Array::Ptr permissions = authenticatedUser->GetPermissions();
+
+ if (permissions) {
+ CpuBoundWork evalPermissions (yc);
+
+ ObjectLock olock(permissions);
+
+ for (const Value& permissionInfo : permissions) {
+ String permission;
+
+ if (permissionInfo.IsObjectType<Dictionary>()) {
+ permission = static_cast<Dictionary::Ptr>(permissionInfo)->Get("permission");
+ } else {
+ permission = permissionInfo;
+ }
+
+ static std::vector<std::pair<String, size_t>> specialContentLengthLimits {
+ { "config/modify", 512 * 1024 * 1024 }
+ };
+
+ for (const auto& limitInfo : specialContentLengthLimits) {
+ if (limitInfo.second <= maxSize) {
+ continue;
+ }
+
+ if (Utility::Match(permission, limitInfo.first)) {
+ maxSize = limitInfo.second;
+ }
+ }
+ }
+ }
+
+ parser.body_limit(maxSize);
+ }
+
+ if (shuttingDown)
+ return false;
+
+ boost::system::error_code ec;
+
+ http::async_read(stream, buf, parser, yc[ec]);
+
+ if (ec) {
+ if (ec == boost::asio::error::operation_aborted)
+ return false;
+
+ /**
+ * Unfortunately there's no way to tell an HTTP protocol error
+ * from an error on a lower layer:
+ *
+ * <https://github.com/boostorg/beast/issues/643>
+ */
+
+ response.result(http::status::bad_request);
+
+ if (parser.get()[http::field::accept] == "application/json") {
+ HttpUtility::SendJsonBody(response, nullptr, new Dictionary({
+ { "error", 400 },
+ { "status", String("Bad Request: ") + ec.message() }
+ }));
+ } else {
+ response.set(http::field::content_type, "text/html");
+ response.body() = String("<h1>Bad Request</h1><p><pre>") + ec.message() + "</pre></p>";
+ response.content_length(response.body().size());
+ }
+
+ response.set(http::field::connection, "close");
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return false;
+ }
+
+ return true;
+}
+
+static inline
+bool ProcessRequest(
+ AsioTlsStream& stream,
+ boost::beast::http::request<boost::beast::http::string_body>& request,
+ ApiUser::Ptr& authenticatedUser,
+ boost::beast::http::response<boost::beast::http::string_body>& response,
+ HttpServerConnection& server,
+ bool& hasStartedStreaming,
+ boost::asio::yield_context& yc
+)
+{
+ namespace http = boost::beast::http;
+
+ try {
+ CpuBoundWork handlingRequest (yc);
+
+ HttpHandler::ProcessRequest(stream, authenticatedUser, request, response, yc, server);
+ } catch (const std::exception& ex) {
+ if (hasStartedStreaming) {
+ return false;
+ }
+
+ auto sysErr (dynamic_cast<const boost::system::system_error*>(&ex));
+
+ if (sysErr && sysErr->code() == boost::asio::error::operation_aborted) {
+ throw;
+ }
+
+ http::response<http::string_body> response;
+
+ HttpUtility::SendJsonError(response, nullptr, 500, "Unhandled exception" , DiagnosticInformation(ex));
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return true;
+ }
+
+ if (hasStartedStreaming) {
+ return false;
+ }
+
+ boost::system::error_code ec;
+
+ http::async_write(stream, response, yc[ec]);
+ stream.async_flush(yc[ec]);
+
+ return true;
+}
+
+void HttpServerConnection::ProcessMessages(boost::asio::yield_context yc)
+{
+ namespace beast = boost::beast;
+ namespace http = beast::http;
+ namespace ch = std::chrono;
+
+ try {
+ /* Do not reset the buffer in the state machine.
+ * EnsureValidHeaders already reads from the stream into the buffer,
+ * EnsureValidBody continues. ProcessRequest() actually handles the request
+ * and needs the full buffer.
+ */
+ beast::flat_buffer buf;
+
+ for (;;) {
+ m_Seen = Utility::GetTime();
+
+ http::parser<true, http::string_body> parser;
+ http::response<http::string_body> response;
+
+ parser.header_limit(1024 * 1024);
+ parser.body_limit(-1);
+
+ response.set(http::field::server, l_ServerHeader);
+
+ if (!EnsureValidHeaders(*m_Stream, buf, parser, response, m_ShuttingDown, yc)) {
+ break;
+ }
+
+ m_Seen = Utility::GetTime();
+ auto start (ch::steady_clock::now());
+
+ auto& request (parser.get());
+
+ {
+ auto method (http::string_to_verb(request["X-Http-Method-Override"]));
+
+ if (method != http::verb::unknown) {
+ request.method(method);
+ }
+ }
+
+ HandleExpect100(*m_Stream, request, yc);
+
+ auto authenticatedUser (m_ApiUser);
+
+ if (!authenticatedUser) {
+ CpuBoundWork fetchingAuthenticatedUser (yc);
+
+ authenticatedUser = ApiUser::GetByAuthHeader(std::string(request[http::field::authorization]));
+ }
+
+ Log logMsg (LogInformation, "HttpServerConnection");
+
+ logMsg << "Request " << request.method_string() << ' ' << request.target()
+ << " (from " << m_PeerAddress
+ << "), user: " << (authenticatedUser ? authenticatedUser->GetName() : "<unauthenticated>")
+ << ", agent: " << request[http::field::user_agent]; //operator[] - Returns the value for a field, or "" if it does not exist.
+
+ Defer addRespCode ([&response, start, &logMsg]() {
+ logMsg << ", status: " << response.result() << ") took "
+ << ch::duration_cast<ch::milliseconds>(ch::steady_clock::now() - start).count() << "ms.";
+ });
+
+ if (!HandleAccessControl(*m_Stream, request, response, yc)) {
+ break;
+ }
+
+ if (!EnsureAcceptHeader(*m_Stream, request, response, yc)) {
+ break;
+ }
+
+ if (!EnsureAuthenticatedUser(*m_Stream, request, authenticatedUser, response, yc)) {
+ break;
+ }
+
+ if (!EnsureValidBody(*m_Stream, buf, parser, authenticatedUser, response, m_ShuttingDown, yc)) {
+ break;
+ }
+
+ m_Seen = std::numeric_limits<decltype(m_Seen)>::max();
+
+ if (!ProcessRequest(*m_Stream, request, authenticatedUser, response, *this, m_HasStartedStreaming, yc)) {
+ break;
+ }
+
+ if (request.version() != 11 || request[http::field::connection] == "close") {
+ break;
+ }
+ }
+ } catch (const std::exception& ex) {
+ if (!m_ShuttingDown) {
+ Log(LogCritical, "HttpServerConnection")
+ << "Unhandled exception while processing HTTP request: " << ex.what();
+ }
+ }
+
+ Disconnect();
+}
+
+void HttpServerConnection::CheckLiveness(boost::asio::yield_context yc)
+{
+ boost::system::error_code ec;
+
+ for (;;) {
+ m_CheckLivenessTimer.expires_from_now(boost::posix_time::seconds(5));
+ m_CheckLivenessTimer.async_wait(yc[ec]);
+
+ if (m_ShuttingDown) {
+ break;
+ }
+
+ if (m_Seen < Utility::GetTime() - 10) {
+ Log(LogInformation, "HttpServerConnection")
+ << "No messages for HTTP connection have been received in the last 10 seconds.";
+
+ Disconnect();
+ break;
+ }
+ }
+}
diff --git a/lib/remote/httpserverconnection.hpp b/lib/remote/httpserverconnection.hpp
new file mode 100644
index 0000000..9c812e5
--- /dev/null
+++ b/lib/remote/httpserverconnection.hpp
@@ -0,0 +1,54 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HTTPSERVERCONNECTION_H
+#define HTTPSERVERCONNECTION_H
+
+#include "remote/apiuser.hpp"
+#include "base/string.hpp"
+#include "base/tlsstream.hpp"
+#include <memory>
+#include <boost/asio/deadline_timer.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/io_context_strand.hpp>
+#include <boost/asio/spawn.hpp>
+
+namespace icinga
+{
+
+/**
+ * An API client connection.
+ *
+ * @ingroup remote
+ */
+class HttpServerConnection final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(HttpServerConnection);
+
+ HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream);
+
+ void Start();
+ void Disconnect();
+ void StartStreaming();
+
+ bool Disconnected();
+
+private:
+ ApiUser::Ptr m_ApiUser;
+ Shared<AsioTlsStream>::Ptr m_Stream;
+ double m_Seen;
+ String m_PeerAddress;
+ boost::asio::io_context::strand m_IoStrand;
+ bool m_ShuttingDown;
+ bool m_HasStartedStreaming;
+ boost::asio::deadline_timer m_CheckLivenessTimer;
+
+ HttpServerConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, boost::asio::io_context& io);
+
+ void ProcessMessages(boost::asio::yield_context yc);
+ void CheckLiveness(boost::asio::yield_context yc);
+};
+
+}
+
+#endif /* HTTPSERVERCONNECTION_H */
diff --git a/lib/remote/httputility.cpp b/lib/remote/httputility.cpp
new file mode 100644
index 0000000..a2142e5
--- /dev/null
+++ b/lib/remote/httputility.cpp
@@ -0,0 +1,80 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/httputility.hpp"
+#include "remote/url.hpp"
+#include "base/json.hpp"
+#include "base/logger.hpp"
+#include <map>
+#include <string>
+#include <vector>
+#include <boost/beast/http.hpp>
+
+using namespace icinga;
+
+Dictionary::Ptr HttpUtility::FetchRequestParameters(const Url::Ptr& url, const std::string& body)
+{
+ Dictionary::Ptr result;
+
+ if (!body.empty()) {
+ Log(LogDebug, "HttpUtility")
+ << "Request body: '" << body << '\'';
+
+ result = JsonDecode(body);
+ }
+
+ if (!result)
+ result = new Dictionary();
+
+ std::map<String, std::vector<String>> query;
+ for (const auto& kv : url->GetQuery()) {
+ query[kv.first].emplace_back(kv.second);
+ }
+
+ for (auto& kv : query) {
+ result->Set(kv.first, Array::FromVector(kv.second));
+ }
+
+ return result;
+}
+
+Value HttpUtility::GetLastParameter(const Dictionary::Ptr& params, const String& key)
+{
+ Value varr = params->Get(key);
+
+ if (!varr.IsObjectType<Array>())
+ return varr;
+
+ Array::Ptr arr = varr;
+
+ if (arr->GetLength() == 0)
+ return Empty;
+ else
+ return arr->Get(arr->GetLength() - 1);
+}
+
+void HttpUtility::SendJsonBody(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const Value& val)
+{
+ namespace http = boost::beast::http;
+
+ response.set(http::field::content_type, "application/json");
+ response.body() = JsonEncode(val, params && GetLastParameter(params, "pretty"));
+ response.content_length(response.body().size());
+}
+
+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);
+}
diff --git a/lib/remote/httputility.hpp b/lib/remote/httputility.hpp
new file mode 100644
index 0000000..6465b4a
--- /dev/null
+++ b/lib/remote/httputility.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef HTTPUTILITY_H
+#define HTTPUTILITY_H
+
+#include "remote/url.hpp"
+#include "base/dictionary.hpp"
+#include <boost/beast/http.hpp>
+#include <string>
+
+namespace icinga
+{
+
+/**
+ * Helper functions.
+ *
+ * @ingroup remote
+ */
+class HttpUtility
+{
+
+public:
+ static Dictionary::Ptr FetchRequestParameters(const Url::Ptr& url, const std::string& body);
+ static Value GetLastParameter(const Dictionary::Ptr& params, const String& key);
+
+ static void SendJsonBody(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const Value& val);
+ static void SendJsonError(boost::beast::http::response<boost::beast::http::string_body>& response, const Dictionary::Ptr& params, const int code,
+ const String& verbose = String(), const String& diagnosticInformation = String());
+};
+
+}
+
+#endif /* HTTPUTILITY_H */
diff --git a/lib/remote/i2-remote.hpp b/lib/remote/i2-remote.hpp
new file mode 100644
index 0000000..5755bef
--- /dev/null
+++ b/lib/remote/i2-remote.hpp
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2REMOTE_H
+#define I2REMOTE_H
+
+/**
+ * @defgroup remote Remote library
+ *
+ * The Icinga library implements remote cluster functionality.
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2REMOTE_H */
diff --git a/lib/remote/infohandler.cpp b/lib/remote/infohandler.cpp
new file mode 100644
index 0000000..80ebba7
--- /dev/null
+++ b/lib/remote/infohandler.cpp
@@ -0,0 +1,100 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/infohandler.hpp"
+#include "remote/httputility.hpp"
+#include "base/application.hpp"
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/", InfoHandler);
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 2)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ if (url->GetPath().empty()) {
+ response.result(http::status::found);
+ response.set(http::field::location, "/v1");
+ return true;
+ }
+
+ if (url->GetPath()[0] != "v1" || url->GetPath().size() != 1)
+ return false;
+
+ response.result(http::status::ok);
+
+ std::vector<String> permInfo;
+ Array::Ptr permissions = user->GetPermissions();
+
+ if (permissions) {
+ ObjectLock olock(permissions);
+ for (const Value& permission : permissions) {
+ String name;
+ bool hasFilter = false;
+ if (permission.IsObjectType<Dictionary>()) {
+ Dictionary::Ptr dpermission = permission;
+ name = dpermission->Get("permission");
+ hasFilter = dpermission->Contains("filter");
+ } else
+ name = permission;
+
+ if (hasFilter)
+ name += " (filtered)";
+
+ permInfo.emplace_back(std::move(name));
+ }
+ }
+
+ if (request[http::field::accept] == "application/json") {
+ Dictionary::Ptr result1 = new Dictionary({
+ { "user", user->GetName() },
+ { "permissions", Array::FromVector(permInfo) },
+ { "version", Application::GetAppVersion() },
+ { "info", "More information about API requests is available in the documentation at https://icinga.com/docs/icinga2/latest/" }
+ });
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ HttpUtility::SendJsonBody(response, params, result);
+ } else {
+ response.set(http::field::content_type, "text/html");
+
+ String body = "<html><head><title>Icinga 2</title></head><h1>Hello from Icinga 2 (Version: " + Application::GetAppVersion() + ")!</h1>";
+ body += "<p>You are authenticated as <b>" + user->GetName() + "</b>. ";
+
+ if (!permInfo.empty()) {
+ body += "Your user has the following permissions:</p> <ul>";
+
+ for (const String& perm : permInfo) {
+ body += "<li>" + perm + "</li>";
+ }
+
+ body += "</ul>";
+ } else
+ body += "Your user does not have any permissions.</p>";
+
+ body += R"(<p>More information about API requests is available in the <a href="https://icinga.com/docs/icinga2/latest/" target="_blank">documentation</a>.</p></html>)";
+ response.body() = body;
+ response.content_length(response.body().size());
+ }
+
+ return true;
+}
+
diff --git a/lib/remote/infohandler.hpp b/lib/remote/infohandler.hpp
new file mode 100644
index 0000000..e1fe983
--- /dev/null
+++ b/lib/remote/infohandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef INFOHANDLER_H
+#define INFOHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class InfoHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(InfoHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* INFOHANDLER_H */
diff --git a/lib/remote/jsonrpc.cpp b/lib/remote/jsonrpc.cpp
new file mode 100644
index 0000000..d4d3d3c
--- /dev/null
+++ b/lib/remote/jsonrpc.cpp
@@ -0,0 +1,157 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/jsonrpc.hpp"
+#include "base/netstring.hpp"
+#include "base/json.hpp"
+#include "base/console.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/convert.hpp"
+#include "base/tlsstream.hpp"
+#include <iostream>
+#include <memory>
+#include <utility>
+#include <boost/asio/spawn.hpp>
+
+using namespace icinga;
+
+#ifdef I2_DEBUG
+/**
+ * Determine whether the developer wants to see raw JSON messages.
+ *
+ * @return Internal.DebugJsonRpc boolean
+ */
+static bool GetDebugJsonRpcCached()
+{
+ static int debugJsonRpc = -1;
+
+ if (debugJsonRpc != -1)
+ return debugJsonRpc;
+
+ debugJsonRpc = false;
+
+ Namespace::Ptr internal = ScriptGlobal::Get("Internal", &Empty);
+
+ if (!internal)
+ return false;
+
+ Value vdebug;
+
+ if (!internal->Get("DebugJsonRpc", &vdebug))
+ return false;
+
+ debugJsonRpc = Convert::ToLong(vdebug);
+
+ return debugJsonRpc;
+}
+#endif /* I2_DEBUG */
+
+/**
+ * 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 Shared<AsioTlsStream>::Ptr& stream, const Dictionary::Ptr& message)
+{
+ String json = JsonEncode(message);
+
+#ifdef I2_DEBUG
+ if (GetDebugJsonRpcCached())
+ std::cerr << ConsoleColorTag(Console_ForegroundBlue) << ">> " << json << ConsoleColorTag(Console_Normal) << "\n";
+#endif /* I2_DEBUG */
+
+ 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 Shared<AsioTlsStream>::Ptr& stream, const Dictionary::Ptr& message, boost::asio::yield_context yc)
+{
+ return JsonRpc::SendRawMessage(stream, JsonEncode(message), yc);
+}
+
+ /**
+ * Sends a raw message to the connected peer.
+ *
+ * @param stream ASIO TLS Stream
+ * @param json message
+ * @param yc Yield context required for ASIO
+ *
+ * @return bytes sent
+ */
+size_t JsonRpc::SendRawMessage(const Shared<AsioTlsStream>::Ptr& stream, const String& json, boost::asio::yield_context yc)
+{
+#ifdef I2_DEBUG
+ if (GetDebugJsonRpcCached())
+ std::cerr << ConsoleColorTag(Console_ForegroundBlue) << ">> " << json << ConsoleColorTag(Console_Normal) << "\n";
+#endif /* I2_DEBUG */
+
+ return NetString::WriteStringToStream(stream, json, yc);
+}
+
+/**
+ * Reads a message from the connected peer.
+ *
+ * @param stream ASIO TLS Stream
+ * @param maxMessageLength maximum size of bytes read.
+ *
+ * @return A JSON string
+ */
+
+String JsonRpc::ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, ssize_t maxMessageLength)
+{
+ String jsonString = NetString::ReadStringFromStream(stream, maxMessageLength);
+
+#ifdef I2_DEBUG
+ if (GetDebugJsonRpcCached())
+ std::cerr << ConsoleColorTag(Console_ForegroundBlue) << "<< " << jsonString << ConsoleColorTag(Console_Normal) << "\n";
+#endif /* I2_DEBUG */
+
+ return jsonString;
+}
+
+/**
+ * Reads a message from the connected peer.
+ *
+ * @param stream ASIO TLS Stream
+ * @param yc Yield Context for ASIO
+ * @param maxMessageLength maximum size of bytes read.
+ *
+ * @return A JSON string
+ */
+String JsonRpc::ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, boost::asio::yield_context yc, ssize_t maxMessageLength)
+{
+ String jsonString = NetString::ReadStringFromStream(stream, yc, maxMessageLength);
+
+#ifdef I2_DEBUG
+ if (GetDebugJsonRpcCached())
+ std::cerr << ConsoleColorTag(Console_ForegroundBlue) << "<< " << jsonString << ConsoleColorTag(Console_Normal) << "\n";
+#endif /* I2_DEBUG */
+
+ return jsonString;
+}
+
+/**
+ * Decode message, enforce a Dictionary
+ *
+ * @param message JSON string
+ *
+ * @return Dictionary ptr
+ */
+Dictionary::Ptr JsonRpc::DecodeMessage(const String& message)
+{
+ Value value = JsonDecode(message);
+
+ if (!value.IsObjectType<Dictionary>()) {
+ BOOST_THROW_EXCEPTION(std::invalid_argument("JSON-RPC"
+ " message must be a dictionary."));
+ }
+
+ return value;
+}
diff --git a/lib/remote/jsonrpc.hpp b/lib/remote/jsonrpc.hpp
new file mode 100644
index 0000000..3f3cdec
--- /dev/null
+++ b/lib/remote/jsonrpc.hpp
@@ -0,0 +1,39 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef JSONRPC_H
+#define JSONRPC_H
+
+#include "base/stream.hpp"
+#include "base/dictionary.hpp"
+#include "base/tlsstream.hpp"
+#include "remote/i2-remote.hpp"
+#include <memory>
+#include <boost/asio/spawn.hpp>
+
+namespace icinga
+{
+
+/**
+ * A JSON-RPC connection.
+ *
+ * @ingroup remote
+ */
+class JsonRpc
+{
+public:
+ static size_t SendMessage(const Shared<AsioTlsStream>::Ptr& stream, const Dictionary::Ptr& message);
+ static size_t SendMessage(const Shared<AsioTlsStream>::Ptr& stream, const Dictionary::Ptr& message, boost::asio::yield_context yc);
+ static size_t SendRawMessage(const Shared<AsioTlsStream>::Ptr& stream, const String& json, boost::asio::yield_context yc);
+
+ static String ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, ssize_t maxMessageLength = -1);
+ static String ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, boost::asio::yield_context yc, ssize_t maxMessageLength = -1);
+
+ static Dictionary::Ptr DecodeMessage(const String& message);
+
+private:
+ JsonRpc();
+};
+
+}
+
+#endif /* JSONRPC_H */
diff --git a/lib/remote/jsonrpcconnection-heartbeat.cpp b/lib/remote/jsonrpcconnection-heartbeat.cpp
new file mode 100644
index 0000000..2474688
--- /dev/null
+++ b/lib/remote/jsonrpcconnection-heartbeat.cpp
@@ -0,0 +1,48 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/messageorigin.hpp"
+#include "remote/apifunction.hpp"
+#include "base/initialize.hpp"
+#include "base/configtype.hpp"
+#include "base/logger.hpp"
+#include "base/utility.hpp"
+#include <boost/asio/spawn.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/system/system_error.hpp>
+
+using namespace icinga;
+
+REGISTER_APIFUNCTION(Heartbeat, event, &JsonRpcConnection::HeartbeatAPIHandler);
+
+/**
+ * We still send a heartbeat without timeout here
+ * to keep the m_Seen variable up to date. This is to keep the
+ * cluster connection alive when there isn't much going on.
+ */
+
+void JsonRpcConnection::HandleAndWriteHeartbeats(boost::asio::yield_context yc)
+{
+ boost::system::error_code ec;
+
+ for (;;) {
+ m_HeartbeatTimer.expires_from_now(boost::posix_time::seconds(20));
+ m_HeartbeatTimer.async_wait(yc[ec]);
+
+ if (m_ShuttingDown) {
+ break;
+ }
+
+ SendMessageInternal(new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "event::Heartbeat" },
+ { "params", new Dictionary() }
+ }));
+ }
+}
+
+Value JsonRpcConnection::HeartbeatAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ return Empty;
+}
+
diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp
new file mode 100644
index 0000000..340e12b
--- /dev/null
+++ b/lib/remote/jsonrpcconnection-pki.cpp
@@ -0,0 +1,439 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/jsonrpc.hpp"
+#include "base/atomic-file.hpp"
+#include "base/configtype.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include <boost/thread/once.hpp>
+#include <boost/regex.hpp>
+#include <fstream>
+#include <openssl/asn1.h>
+#include <openssl/ssl.h>
+#include <openssl/x509.h>
+
+using namespace icinga;
+
+static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
+static Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+REGISTER_APIFUNCTION(UpdateCertificate, pki, &UpdateCertificateHandler);
+
+Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ String certText = params->Get("cert_request");
+
+ std::shared_ptr<X509> cert;
+
+ Dictionary::Ptr result = new Dictionary();
+ auto& tlsConn (origin->FromClient->GetStream()->next_layer());
+
+ /* Use the presented client certificate if not provided. */
+ if (certText.IsEmpty()) {
+ cert = tlsConn.GetPeerCertificate();
+ } else {
+ cert = StringToCertificate(certText);
+ }
+
+ if (!cert) {
+ Log(LogWarning, "JsonRpcConnection") << "No certificate or CSR received";
+
+ result->Set("status_code", 1);
+ result->Set("error", "No certificate or CSR received.");
+
+ return result;
+ }
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+ std::shared_ptr<X509> cacert = GetX509Certificate(listener->GetDefaultCaPath());
+
+ String cn = GetCertificateCN(cert);
+
+ bool signedByCA = false;
+
+ {
+ Log logmsg(LogInformation, "JsonRpcConnection");
+ logmsg << "Received certificate request for CN '" << cn << "'";
+
+ try {
+ signedByCA = VerifyCertificate(cacert, cert, listener->GetCrlPath());
+ if (!signedByCA) {
+ logmsg << " not";
+ }
+ logmsg << " signed by our CA.";
+ } catch (const std::exception &ex) {
+ logmsg << " which couldn't be verified";
+
+ if (const unsigned long *openssl_code = boost::get_error_info<errinfo_openssl_error>(ex)) {
+ logmsg << ": " << X509_verify_cert_error_string(long(*openssl_code)) << " (code " << *openssl_code << ")";
+ } else {
+ logmsg << ".";
+ }
+ }
+ }
+
+ std::shared_ptr<X509> parsedRequestorCA;
+ X509* requestorCA = nullptr;
+
+ if (signedByCA) {
+ bool uptodate = IsCertUptodate(cert);
+
+ if (uptodate) {
+ // Even if the leaf is up-to-date, the root may expire soon.
+ // In a regular setup where Icinga manages the PKI, there is only one CA.
+ // Icinga includes it in handshakes, let's see whether the peer needs a fresh one...
+
+ if (cn == origin->FromClient->GetIdentity()) {
+ auto chain (SSL_get_peer_cert_chain(tlsConn.native_handle()));
+
+ if (chain) {
+ auto len (sk_X509_num(chain));
+
+ for (int i = 0; i < len; ++i) {
+ auto link (sk_X509_value(chain, i));
+
+ if (!X509_NAME_cmp(X509_get_subject_name(link), X509_get_issuer_name(link))) {
+ requestorCA = link;
+ }
+ }
+ }
+ } else {
+ Value requestorCaStr;
+
+ if (params->Get("requestor_ca", &requestorCaStr)) {
+ parsedRequestorCA = StringToCertificate(requestorCaStr);
+ requestorCA = parsedRequestorCA.get();
+ }
+ }
+
+ if (requestorCA && !IsCaUptodate(requestorCA)) {
+ int days;
+
+ if (ASN1_TIME_diff(&days, nullptr, X509_get_notAfter(requestorCA), X509_get_notAfter(cacert.get())) && days > 0) {
+ uptodate = false;
+ }
+ }
+ }
+
+ if (uptodate) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "The certificates for CN '" << cn << "' and its root CA are valid and uptodate. Skipping automated renewal.";
+ result->Set("status_code", 1);
+ result->Set("error", "The certificates for CN '" + cn + "' and its root CA are valid and uptodate. Skipping automated renewal.");
+ return result;
+ }
+ }
+
+ unsigned int n;
+ unsigned char digest[EVP_MAX_MD_SIZE];
+
+ if (!X509_digest(cert.get(), EVP_sha256(), digest, &n)) {
+ result->Set("status_code", 1);
+ result->Set("error", "Could not calculate fingerprint for the X509 certificate for CN '" + cn + "'.");
+
+ Log(LogWarning, "JsonRpcConnection")
+ << "Could not calculate fingerprint for the X509 certificate requested for CN '"
+ << cn << "'.";
+
+ return result;
+ }
+
+ char certFingerprint[EVP_MAX_MD_SIZE*2+1];
+ for (unsigned int i = 0; i < n; i++)
+ sprintf(certFingerprint + 2 * i, "%02x", digest[i]);
+
+ result->Set("fingerprint_request", certFingerprint);
+
+ String requestDir = ApiListener::GetCertificateRequestsDir();
+ String requestPath = requestDir + "/" + certFingerprint + ".json";
+
+ result->Set("ca", CertificateToString(cacert));
+
+ JsonRpcConnection::Ptr client = origin->FromClient;
+
+ /* If we already have a signed certificate request, send it to the client. */
+ if (Utility::PathExists(requestPath)) {
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
+
+ String certResponse = request->Get("cert_response");
+
+ if (!certResponse.IsEmpty()) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "Sending certificate response for CN '" << cn
+ << "' to endpoint '" << client->GetIdentity() << "'.";
+
+ result->Set("cert", certResponse);
+ result->Set("status_code", 0);
+
+ Dictionary::Ptr message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "pki::UpdateCertificate" },
+ { "params", result }
+ });
+ client->SendMessage(message);
+
+ return result;
+ }
+ } else if (Utility::PathExists(requestDir + "/" + certFingerprint + ".removed")) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "Certificate for CN " << cn << " has been removed. Ignoring signing request.";
+ result->Set("status_code", 1);
+ result->Set("error", "Ticket for CN " + cn + " declined by administrator.");
+ return result;
+ }
+
+ std::shared_ptr<X509> newcert;
+ Dictionary::Ptr message;
+ String ticket;
+
+ /* Check whether we are a signing instance or we
+ * must delay the signing request.
+ */
+ if (!Utility::PathExists(GetIcingaCADir() + "/ca.key"))
+ goto delayed_request;
+
+ if (!signedByCA) {
+ String salt = listener->GetTicketSalt();
+
+ ticket = params->Get("ticket");
+
+ // Auto-signing is disabled: Client did not include a ticket in its request.
+ if (ticket.IsEmpty()) {
+ Log(LogNotice, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn
+ << "': No ticket included, skipping auto-signing and waiting for on-demand signing approval.";
+
+ goto delayed_request;
+ }
+
+ // Auto-signing is disabled: no TicketSalt
+ if (salt.IsEmpty()) {
+ Log(LogNotice, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn
+ << "': This instance is the signing master for the Icinga CA."
+ << " The 'ticket_salt' attribute in the 'api' feature is not set."
+ << " Not signing the request. Please check the docs.";
+
+ goto delayed_request;
+ }
+
+ String realTicket = PBKDF2_SHA1(cn, salt, 50000);
+
+ Log(LogDebug, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn << "': Comparing received ticket '"
+ << ticket << "' with calculated ticket '" << realTicket << "'.";
+
+ if (!Utility::ComparePasswords(ticket, realTicket)) {
+ Log(LogWarning, "JsonRpcConnection")
+ << "Ticket '" << ticket << "' for CN '" << cn << "' is invalid.";
+
+ result->Set("status_code", 1);
+ result->Set("error", "Invalid ticket for CN '" + cn + "'.");
+ return result;
+ }
+ }
+
+ newcert = listener->RenewCert(cert);
+
+ if (!newcert) {
+ goto delayed_request;
+ }
+
+ /* Send the signed certificate update. */
+ Log(LogInformation, "JsonRpcConnection")
+ << "Sending certificate response for CN '" << cn << "' to endpoint '"
+ << client->GetIdentity() << "'" << (!ticket.IsEmpty() ? " (auto-signing ticket)" : "" ) << ".";
+
+ result->Set("cert", CertificateToString(newcert));
+
+ result->Set("status_code", 0);
+
+ message = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "method", "pki::UpdateCertificate" },
+ { "params", result }
+ });
+ client->SendMessage(message);
+
+ return result;
+
+delayed_request:
+ /* Send a delayed certificate signing request. */
+ Utility::MkDirP(requestDir, 0700);
+
+ Dictionary::Ptr request = new Dictionary({
+ { "cert_request", CertificateToString(cert) },
+ { "ticket", params->Get("ticket") }
+ });
+
+ if (requestorCA) {
+ request->Set("requestor_ca", CertificateToString(requestorCA));
+ }
+
+ Utility::SaveJsonFile(requestPath, 0600, request);
+
+ JsonRpcConnection::SendCertificateRequest(nullptr, origin, requestPath);
+
+ result->Set("status_code", 2);
+ result->Set("error", "Certificate request for CN '" + cn + "' is pending. Waiting for approval from the parent Icinga instance.");
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Certificate request for CN '" << cn << "' is pending. Waiting for approval.";
+
+ if (origin) {
+ auto client (origin->FromClient);
+
+ if (client && !client->GetEndpoint()) {
+ client->Disconnect();
+ }
+ }
+
+ return result;
+}
+
+void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const MessageOrigin::Ptr& origin, const String& path)
+{
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "pki::RequestCertificate");
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return;
+
+ Dictionary::Ptr params = new Dictionary();
+ message->Set("params", params);
+
+ /* Path is empty if this is our own request. */
+ if (path.IsEmpty()) {
+ {
+ Log msg (LogInformation, "JsonRpcConnection");
+ msg << "Requesting new certificate for this Icinga instance";
+
+ if (aclient) {
+ msg << " from endpoint '" << aclient->GetIdentity() << "'";
+ }
+
+ msg << ".";
+ }
+
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+
+ std::ifstream fp(ticketPath.CStr());
+ String ticket((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
+ fp.close();
+
+ params->Set("ticket", ticket);
+ } else {
+ Dictionary::Ptr request = Utility::LoadJsonFile(path);
+
+ if (request->Contains("cert_response"))
+ return;
+
+ request->CopyTo(params);
+ }
+
+ /* Send the request to a) the connected client
+ * or b) the local zone and all parents.
+ */
+ if (aclient)
+ aclient->SendMessage(message);
+ else
+ listener->RelayMessage(origin, Zone::GetLocalZone(), message, false);
+}
+
+Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ if (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)) {
+ Log(LogWarning, "ClusterEvents")
+ << "Discarding 'update certificate' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ String ca = params->Get("ca");
+ String cert = params->Get("cert");
+
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+
+ if (!listener)
+ return Empty;
+
+ std::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetDefaultCertPath());
+ std::shared_ptr<X509> newCert = StringToCertificate(cert);
+
+ String cn = GetCertificateCN(newCert);
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Received certificate update message for CN '" << cn << "'";
+
+ /* Check if this is a certificate update for a subordinate instance. */
+ std::shared_ptr<EVP_PKEY> oldKey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(oldCert.get()), EVP_PKEY_free);
+ std::shared_ptr<EVP_PKEY> newKey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(newCert.get()), EVP_PKEY_free);
+
+ if (X509_NAME_cmp(X509_get_subject_name(oldCert.get()), X509_get_subject_name(newCert.get())) != 0 ||
+ EVP_PKEY_cmp(oldKey.get(), newKey.get()) != 1) {
+ String certFingerprint = params->Get("fingerprint_request");
+
+ /* Validate the fingerprint format. */
+ boost::regex expr("^[0-9a-f]+$");
+
+ if (!boost::regex_match(certFingerprint.GetData(), expr)) {
+ Log(LogWarning, "JsonRpcConnection")
+ << "Endpoint '" << origin->FromClient->GetIdentity() << "' sent an invalid certificate fingerprint: '"
+ << certFingerprint << "' for CN '" << cn << "'.";
+ return Empty;
+ }
+
+ String requestDir = ApiListener::GetCertificateRequestsDir();
+ String requestPath = requestDir + "/" + certFingerprint + ".json";
+
+ /* Save the received signed certificate request to disk. */
+ if (Utility::PathExists(requestPath)) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "Saved certificate update for CN '" << cn << "'";
+
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
+ request->Set("cert_response", cert);
+ Utility::SaveJsonFile(requestPath, 0644, request);
+ }
+
+ return Empty;
+ }
+
+ /* Update CA certificate. */
+ String caPath = listener->GetDefaultCaPath();
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Updating CA certificate in '" << caPath << "'.";
+
+ AtomicFile::Write(caPath, 0644, ca);
+
+ /* Update signed certificate. */
+ String certPath = listener->GetDefaultCertPath();
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Updating client certificate for CN '" << cn << "' in '" << certPath << "'.";
+
+ AtomicFile::Write(certPath, 0644, cert);
+
+ /* Remove ticket for successful signing request. */
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+
+ Utility::Remove(ticketPath);
+
+ /* Update the certificates at runtime and reconnect all endpoints. */
+ Log(LogInformation, "JsonRpcConnection")
+ << "Updating the client certificate for CN '" << cn << "' at runtime and reconnecting the endpoints.";
+
+ listener->UpdateSSLContext();
+
+ return Empty;
+}
diff --git a/lib/remote/jsonrpcconnection.cpp b/lib/remote/jsonrpcconnection.cpp
new file mode 100644
index 0000000..3bae3ca
--- /dev/null
+++ b/lib/remote/jsonrpcconnection.cpp
@@ -0,0 +1,388 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/jsonrpcconnection.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/apifunction.hpp"
+#include "remote/jsonrpc.hpp"
+#include "base/defer.hpp"
+#include "base/configtype.hpp"
+#include "base/io-engine.hpp"
+#include "base/json.hpp"
+#include "base/objectlock.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include "base/tlsstream.hpp"
+#include <memory>
+#include <utility>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/system/system_error.hpp>
+#include <boost/thread/once.hpp>
+
+using namespace icinga;
+
+static Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler);
+
+static RingBuffer l_TaskStats (15 * 60);
+
+JsonRpcConnection::JsonRpcConnection(const String& identity, bool authenticated,
+ const Shared<AsioTlsStream>::Ptr& stream, ConnectionRole role)
+ : JsonRpcConnection(identity, authenticated, stream, role, IoEngine::Get().GetIoContext())
+{
+}
+
+JsonRpcConnection::JsonRpcConnection(const String& identity, bool authenticated,
+ const Shared<AsioTlsStream>::Ptr& stream, ConnectionRole role, boost::asio::io_context& io)
+ : m_Identity(identity), m_Authenticated(authenticated), m_Stream(stream), m_Role(role),
+ m_Timestamp(Utility::GetTime()), m_Seen(Utility::GetTime()), m_NextHeartbeat(0), m_IoStrand(io),
+ m_OutgoingMessagesQueued(io), m_WriterDone(io), m_ShuttingDown(false),
+ m_CheckLivenessTimer(io), m_HeartbeatTimer(io)
+{
+ if (authenticated)
+ m_Endpoint = Endpoint::GetByName(identity);
+}
+
+void JsonRpcConnection::Start()
+{
+ namespace asio = boost::asio;
+
+ JsonRpcConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { HandleIncomingMessages(yc); });
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { WriteOutgoingMessages(yc); });
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { HandleAndWriteHeartbeats(yc); });
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) { CheckLiveness(yc); });
+}
+
+void JsonRpcConnection::HandleIncomingMessages(boost::asio::yield_context yc)
+{
+ m_Stream->next_layer().SetSeen(&m_Seen);
+
+ for (;;) {
+ String message;
+
+ try {
+ message = JsonRpc::ReadMessage(m_Stream, yc, m_Endpoint ? -1 : 1024 * 1024);
+ } catch (const std::exception& ex) {
+ Log(m_ShuttingDown ? LogDebug : LogNotice, "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) {
+ Log(m_ShuttingDown ? LogDebug : LogWarning, "JsonRpcConnection")
+ << "Error while processing JSON-RPC message for identity '" << m_Identity
+ << "': " << DiagnosticInformation(ex);
+
+ break;
+ }
+
+ CpuBoundWork taskStats (yc);
+
+ l_TaskStats.InsertValue(Utility::GetTime(), 1);
+ }
+
+ Disconnect();
+}
+
+void JsonRpcConnection::WriteOutgoingMessages(boost::asio::yield_context yc)
+{
+ 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) {
+ Log(m_ShuttingDown ? LogDebug : LogWarning, "JsonRpcConnection")
+ << "Error while sending JSON-RPC message for identity '"
+ << m_Identity << "'\n" << DiagnosticInformation(ex);
+
+ break;
+ }
+ }
+ } while (!m_ShuttingDown);
+
+ Disconnect();
+}
+
+double JsonRpcConnection::GetTimestamp() const
+{
+ return m_Timestamp;
+}
+
+String JsonRpcConnection::GetIdentity() const
+{
+ return m_Identity;
+}
+
+bool JsonRpcConnection::IsAuthenticated() const
+{
+ return m_Authenticated;
+}
+
+Endpoint::Ptr JsonRpcConnection::GetEndpoint() const
+{
+ return m_Endpoint;
+}
+
+Shared<AsioTlsStream>::Ptr JsonRpcConnection::GetStream() const
+{
+ return m_Stream;
+}
+
+ConnectionRole JsonRpcConnection::GetRole() const
+{
+ return m_Role;
+}
+
+void JsonRpcConnection::SendMessage(const Dictionary::Ptr& message)
+{
+ Ptr keepAlive (this);
+
+ m_IoStrand.post([this, keepAlive, message]() { SendMessageInternal(message); });
+}
+
+void JsonRpcConnection::SendRawMessage(const String& message)
+{
+ Ptr keepAlive (this);
+
+ m_IoStrand.post([this, keepAlive, 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()
+{
+ namespace asio = boost::asio;
+
+ JsonRpcConnection::Ptr keepAlive (this);
+
+ IoEngine::SpawnCoroutine(m_IoStrand, [this, keepAlive](asio::yield_context yc) {
+ if (!m_ShuttingDown) {
+ m_ShuttingDown = true;
+
+ Log(LogWarning, "JsonRpcConnection")
+ << "API client disconnected for identity '" << m_Identity << "'";
+
+ {
+ CpuBoundWork removeClient (yc);
+
+ if (m_Endpoint) {
+ m_Endpoint->RemoveClient(this);
+ } else {
+ ApiListener::GetInstance()->RemoveAnonymousClient(this);
+ }
+ }
+
+ m_OutgoingMessagesQueued.Set();
+
+ m_WriterDone.Wait(yc);
+
+ /*
+ * Do not swallow exceptions in a coroutine.
+ * https://github.com/Icinga/icinga2/issues/7351
+ * We must not catch `detail::forced_unwind exception` as
+ * this is used for unwinding the stack.
+ *
+ * Just use the error_code dummy here.
+ */
+ boost::system::error_code ec;
+
+ m_CheckLivenessTimer.cancel();
+ m_HeartbeatTimer.cancel();
+
+ m_Stream->lowest_layer().cancel(ec);
+
+ Timeout::Ptr shutdownTimeout (new Timeout(
+ m_IoStrand.context(),
+ m_IoStrand,
+ boost::posix_time::seconds(10),
+ [this, keepAlive](asio::yield_context yc) {
+ boost::system::error_code ec;
+ m_Stream->lowest_layer().cancel(ec);
+ }
+ ));
+
+ m_Stream->next_layer().async_shutdown(yc[ec]);
+
+ shutdownTimeout->Cancel();
+
+ m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both, ec);
+ }
+ });
+}
+
+void JsonRpcConnection::MessageHandler(const String& jsonString)
+{
+ Dictionary::Ptr message = JsonRpc::DecodeMessage(jsonString);
+
+ if (m_Endpoint && message->Contains("ts")) {
+ double ts = message->Get("ts");
+
+ /* ignore old messages */
+ if (ts < m_Endpoint->GetRemoteLogPosition())
+ return;
+
+ m_Endpoint->SetRemoteLogPosition(ts);
+ }
+
+ MessageOrigin::Ptr origin = new MessageOrigin();
+ origin->FromClient = this;
+
+ if (m_Endpoint) {
+ if (m_Endpoint->GetZone() != Zone::GetLocalZone())
+ origin->FromZone = m_Endpoint->GetZone();
+ else
+ origin->FromZone = Zone::GetByName(message->Get("originZone"));
+
+ m_Endpoint->AddMessageReceived(jsonString.GetLength());
+ }
+
+ Value vmethod;
+
+ if (!message->Get("method", &vmethod)) {
+ Value vid;
+
+ if (!message->Get("id", &vid))
+ return;
+
+ Log(LogWarning, "JsonRpcConnection",
+ "We received a JSON-RPC response message. This should never happen because we're only ever sending notifications.");
+
+ return;
+ }
+
+ String method = vmethod;
+
+ Log(LogNotice, "JsonRpcConnection")
+ << "Received '" << method << "' message from identity '" << m_Identity << "'.";
+
+ Dictionary::Ptr resultMessage = new Dictionary();
+
+ try {
+ ApiFunction::Ptr afunc = ApiFunction::GetByName(method);
+
+ if (!afunc) {
+ Log(LogNotice, "JsonRpcConnection")
+ << "Call to non-existent function '" << method << "' from endpoint '" << m_Identity << "'.";
+ } else {
+ Dictionary::Ptr params = message->Get("params");
+ if (params)
+ resultMessage->Set("result", afunc->Invoke(origin, params));
+ else
+ resultMessage->Set("result", Empty);
+ }
+ } catch (const std::exception& ex) {
+ /* TODO: Add a user readable error message for the remote caller */
+ String diagInfo = DiagnosticInformation(ex);
+ resultMessage->Set("error", diagInfo);
+ Log(LogWarning, "JsonRpcConnection")
+ << "Error while processing message for identity '" << m_Identity << "'\n" << diagInfo;
+ }
+
+ if (message->Contains("id")) {
+ resultMessage->Set("jsonrpc", "2.0");
+ resultMessage->Set("id", message->Get("id"));
+
+ SendMessageInternal(resultMessage);
+ }
+}
+
+Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ double log_position = params->Get("log_position");
+ Endpoint::Ptr endpoint = origin->FromClient->GetEndpoint();
+
+ if (!endpoint)
+ return Empty;
+
+ if (log_position > endpoint->GetLocalLogPosition())
+ endpoint->SetLocalLogPosition(log_position);
+
+ return Empty;
+}
+
+void JsonRpcConnection::CheckLiveness(boost::asio::yield_context yc)
+{
+ boost::system::error_code ec;
+
+ if (!m_Authenticated) {
+ /* Anonymous connections are normally only used for requesting a certificate and are closed after this request
+ * is received. However, the request is only sent if the child has successfully verified the certificate of its
+ * parent so that it is an authenticated connection from its perspective. In case this verification fails, both
+ * ends view it as an anonymous connection and never actually use it but attempt a reconnect after 10 seconds
+ * leaking the connection. Therefore close it after a timeout.
+ */
+
+ m_CheckLivenessTimer.expires_from_now(boost::posix_time::seconds(10));
+ m_CheckLivenessTimer.async_wait(yc[ec]);
+
+ if (m_ShuttingDown) {
+ return;
+ }
+
+ auto remote (m_Stream->lowest_layer().remote_endpoint());
+
+ Log(LogInformation, "JsonRpcConnection")
+ << "Closing anonymous connection [" << remote.address() << "]:" << remote.port() << " after 10 seconds.";
+
+ Disconnect();
+ } else {
+ for (;;) {
+ m_CheckLivenessTimer.expires_from_now(boost::posix_time::seconds(30));
+ m_CheckLivenessTimer.async_wait(yc[ec]);
+
+ if (m_ShuttingDown) {
+ break;
+ }
+
+ if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) {
+ Log(LogInformation, "JsonRpcConnection")
+ << "No messages for identity '" << m_Identity << "' have been received in the last 60 seconds.";
+
+ Disconnect();
+ break;
+ }
+ }
+ }
+}
+
+double JsonRpcConnection::GetWorkQueueRate()
+{
+ return l_TaskStats.UpdateAndGetValues(Utility::GetTime(), 60) / 60.0;
+}
diff --git a/lib/remote/jsonrpcconnection.hpp b/lib/remote/jsonrpcconnection.hpp
new file mode 100644
index 0000000..591ddcb
--- /dev/null
+++ b/lib/remote/jsonrpcconnection.hpp
@@ -0,0 +1,100 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef JSONRPCCONNECTION_H
+#define JSONRPCCONNECTION_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/endpoint.hpp"
+#include "base/io-engine.hpp"
+#include "base/tlsstream.hpp"
+#include "base/timer.hpp"
+#include "base/workqueue.hpp"
+#include <memory>
+#include <vector>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/io_context_strand.hpp>
+#include <boost/asio/spawn.hpp>
+
+namespace icinga
+{
+
+enum ClientRole
+{
+ ClientInbound,
+ ClientOutbound
+};
+
+enum ClientType
+{
+ ClientJsonRpc,
+ ClientHttp
+};
+
+class MessageOrigin;
+
+/**
+ * An API client connection.
+ *
+ * @ingroup remote
+ */
+class JsonRpcConnection final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(JsonRpcConnection);
+
+ JsonRpcConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, ConnectionRole role);
+
+ void Start();
+
+ double GetTimestamp() const;
+ String GetIdentity() const;
+ bool IsAuthenticated() const;
+ Endpoint::Ptr GetEndpoint() const;
+ Shared<AsioTlsStream>::Ptr GetStream() const;
+ ConnectionRole GetRole() const;
+
+ void Disconnect();
+
+ void SendMessage(const Dictionary::Ptr& request);
+ void SendRawMessage(const String& request);
+
+ static Value HeartbeatAPIHandler(const intrusive_ptr<MessageOrigin>& origin, const Dictionary::Ptr& params);
+
+ static double GetWorkQueueRate();
+
+ static void SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const intrusive_ptr<MessageOrigin>& origin, const String& path);
+
+private:
+ String m_Identity;
+ bool m_Authenticated;
+ Endpoint::Ptr m_Endpoint;
+ Shared<AsioTlsStream>::Ptr m_Stream;
+ ConnectionRole m_Role;
+ double m_Timestamp;
+ double m_Seen;
+ double m_NextHeartbeat;
+ boost::asio::io_context::strand m_IoStrand;
+ std::vector<String> m_OutgoingMessagesQueue;
+ AsioConditionVariable m_OutgoingMessagesQueued;
+ AsioConditionVariable m_WriterDone;
+ bool m_ShuttingDown;
+ boost::asio::deadline_timer m_CheckLivenessTimer, m_HeartbeatTimer;
+
+ JsonRpcConnection(const String& identity, bool authenticated, const Shared<AsioTlsStream>::Ptr& stream, ConnectionRole role, boost::asio::io_context& io);
+
+ void HandleIncomingMessages(boost::asio::yield_context yc);
+ void WriteOutgoingMessages(boost::asio::yield_context yc);
+ void HandleAndWriteHeartbeats(boost::asio::yield_context yc);
+ void CheckLiveness(boost::asio::yield_context yc);
+
+ bool ProcessMessage();
+ void MessageHandler(const String& jsonString);
+
+ void CertificateRequestResponseHandler(const Dictionary::Ptr& message);
+
+ void SendMessageInternal(const Dictionary::Ptr& request);
+};
+
+}
+
+#endif /* JSONRPCCONNECTION_H */
diff --git a/lib/remote/messageorigin.cpp b/lib/remote/messageorigin.cpp
new file mode 100644
index 0000000..7de0ca7
--- /dev/null
+++ b/lib/remote/messageorigin.cpp
@@ -0,0 +1,10 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/messageorigin.hpp"
+
+using namespace icinga;
+
+bool MessageOrigin::IsLocal() const
+{
+ return !FromClient;
+}
diff --git a/lib/remote/messageorigin.hpp b/lib/remote/messageorigin.hpp
new file mode 100644
index 0000000..8a91ecc
--- /dev/null
+++ b/lib/remote/messageorigin.hpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MESSAGEORIGIN_H
+#define MESSAGEORIGIN_H
+
+#include "remote/zone.hpp"
+#include "remote/jsonrpcconnection.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup remote
+ */
+class MessageOrigin final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(MessageOrigin);
+
+ Zone::Ptr FromZone;
+ JsonRpcConnection::Ptr FromClient;
+
+ bool IsLocal() const;
+};
+
+}
+
+#endif /* MESSAGEORIGIN_H */
diff --git a/lib/remote/modifyobjecthandler.cpp b/lib/remote/modifyobjecthandler.cpp
new file mode 100644
index 0000000..d6fa98b
--- /dev/null
+++ b/lib/remote/modifyobjecthandler.cpp
@@ -0,0 +1,168 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/modifyobjecthandler.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "remote/apiaction.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/objects", ModifyObjectHandler);
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() != http::verb::post)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ QueryDescription qd;
+ qd.Types.insert(type->GetName());
+ qd.Permission = "objects/modify/" + type->GetName();
+
+ params->Set("type", type->GetName());
+
+ if (url->GetPath().size() >= 4) {
+ String attr = type->GetName();
+ boost::algorithm::to_lower(attr);
+ params->Set(attr, url->GetPath()[3]);
+ }
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ Value attrsVal = params->Get("attrs");
+
+ if (attrsVal.GetReflectionType() != Dictionary::TypeInstance && attrsVal.GetType() != ValueEmpty) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'attrs' attribute specified. Dictionary type is required."
+ "Or is this a POST query and you missed adding a 'X-HTTP-Method-Override: GET' header?");
+ return true;
+ }
+
+ Dictionary::Ptr attrs = attrsVal;
+
+ Value restoreAttrsVal = params->Get("restore_attrs");
+
+ if (restoreAttrsVal.GetReflectionType() != Array::TypeInstance && restoreAttrsVal.GetType() != ValueEmpty) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'restore_attrs' attribute specified. Array type is required.");
+ return true;
+ }
+
+ Array::Ptr restoreAttrs = restoreAttrsVal;
+
+ if (!(attrs || restoreAttrs)) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Missing both 'attrs' and 'restore_attrs'. "
+ "Or is this a POST query and you missed adding a 'X-HTTP-Method-Override: GET' header?");
+ return true;
+ }
+
+ bool verbose = false;
+
+ if (params)
+ verbose = HttpUtility::GetLastParameter(params, "verbose");
+
+ ConfigObjectsSharedLock lock (std::try_to_lock);
+
+ if (!lock) {
+ HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
+ return true;
+ }
+
+ ArrayData results;
+
+ for (const ConfigObject::Ptr& obj : objs) {
+ Dictionary::Ptr result1 = new Dictionary();
+
+ result1->Set("type", type->GetName());
+ result1->Set("name", obj->GetName());
+
+ String key;
+
+ try {
+ if (restoreAttrs) {
+ ObjectLock oLock (restoreAttrs);
+
+ for (auto& attr : restoreAttrs) {
+ key = attr;
+ obj->RestoreAttribute(key);
+ }
+ }
+ } catch (const std::exception& ex) {
+ result1->Set("code", 500);
+ result1->Set("status", "Attribute '" + key + "' could not be restored: " + DiagnosticInformation(ex, false));
+
+ if (verbose)
+ result1->Set("diagnostic_information", DiagnosticInformation(ex));
+
+ results.push_back(std::move(result1));
+ continue;
+ }
+
+ try {
+ if (attrs) {
+ ObjectLock olock(attrs);
+ for (const Dictionary::Pair& kv : attrs) {
+ key = kv.first;
+ obj->ModifyAttribute(kv.first, kv.second);
+ }
+ }
+ } catch (const std::exception& ex) {
+ result1->Set("code", 500);
+ result1->Set("status", "Attribute '" + key + "' could not be set: " + DiagnosticInformation(ex, false));
+
+ if (verbose)
+ result1->Set("diagnostic_information", DiagnosticInformation(ex));
+
+ results.push_back(std::move(result1));
+ continue;
+ }
+
+ result1->Set("code", 200);
+ result1->Set("status", "Attributes updated.");
+
+ results.push_back(std::move(result1));
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/modifyobjecthandler.hpp b/lib/remote/modifyobjecthandler.hpp
new file mode 100644
index 0000000..f469301
--- /dev/null
+++ b/lib/remote/modifyobjecthandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef MODIFYOBJECTHANDLER_H
+#define MODIFYOBJECTHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ModifyObjectHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ModifyObjectHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* MODIFYOBJECTHANDLER_H */
diff --git a/lib/remote/objectqueryhandler.cpp b/lib/remote/objectqueryhandler.cpp
new file mode 100644
index 0000000..ad73030
--- /dev/null
+++ b/lib/remote/objectqueryhandler.cpp
@@ -0,0 +1,330 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/objectqueryhandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/serializer.hpp"
+#include "base/dependencygraph.hpp"
+#include "base/configtype.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <set>
+#include <unordered_map>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/objects", ObjectQueryHandler);
+
+Dictionary::Ptr ObjectQueryHandler::SerializeObjectAttrs(const Object::Ptr& object,
+ const String& attrPrefix, const Array::Ptr& attrs, bool isJoin, bool allAttrs)
+{
+ Type::Ptr type = object->GetReflectionType();
+
+ std::vector<int> fids;
+
+ if (isJoin && attrs) {
+ ObjectLock olock(attrs);
+ for (const String& attr : attrs) {
+ if (attr == attrPrefix) {
+ allAttrs = true;
+ break;
+ }
+ }
+ }
+
+ if (!isJoin && (!attrs || attrs->GetLength() == 0))
+ allAttrs = true;
+
+ if (allAttrs) {
+ for (int fid = 0; fid < type->GetFieldCount(); fid++) {
+ fids.push_back(fid);
+ }
+ } else if (attrs) {
+ ObjectLock olock(attrs);
+ for (const String& attr : attrs) {
+ String userAttr;
+
+ if (isJoin) {
+ String::SizeType dpos = attr.FindFirstOf(".");
+ if (dpos == String::NPos)
+ continue;
+
+ String userJoinAttr = attr.SubStr(0, dpos);
+ if (userJoinAttr != attrPrefix)
+ continue;
+
+ userAttr = attr.SubStr(dpos + 1);
+ } else
+ userAttr = attr;
+
+ int fid = type->GetFieldId(userAttr);
+
+ if (fid < 0)
+ BOOST_THROW_EXCEPTION(ScriptError("Invalid field specified: " + userAttr));
+
+ fids.push_back(fid);
+ }
+ }
+
+ DictionaryData resultAttrs;
+ resultAttrs.reserve(fids.size());
+
+ for (int fid : fids) {
+ Field field = type->GetFieldInfo(fid);
+
+ Value val = object->GetField(fid);
+
+ /* hide attributes which shouldn't be user-visible */
+ if (field.Attributes & FANoUserView)
+ continue;
+
+ /* hide internal navigation fields */
+ if (field.Attributes & FANavigation && !(field.Attributes & (FAConfig | FAState)))
+ continue;
+
+ Value sval = Serialize(val, FAConfig | FAState);
+ resultAttrs.emplace_back(field.Name, sval);
+ }
+
+ return new Dictionary(std::move(resultAttrs));
+}
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ QueryDescription qd;
+ qd.Types.insert(type->GetName());
+ qd.Permission = "objects/query/" + type->GetName();
+
+ Array::Ptr uattrs, ujoins, umetas;
+
+ try {
+ uattrs = params->Get("attrs");
+ } catch (const std::exception&) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'attrs' attribute specified. Array type is required.");
+ return true;
+ }
+
+ try {
+ ujoins = params->Get("joins");
+ } catch (const std::exception&) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'joins' attribute specified. Array type is required.");
+ return true;
+ }
+
+ try {
+ umetas = params->Get("meta");
+ } catch (const std::exception&) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid type for 'meta' attribute specified. Array type is required.");
+ return true;
+ }
+
+ bool allJoins = HttpUtility::GetLastParameter(params, "all_joins");
+
+ params->Set("type", type->GetName());
+
+ if (url->GetPath().size() >= 4) {
+ String attr = type->GetName();
+ boost::algorithm::to_lower(attr);
+ params->Set(attr, url->GetPath()[3]);
+ }
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ ArrayData results;
+ results.reserve(objs.size());
+
+ std::set<String> joinAttrs;
+ std::set<String> userJoinAttrs;
+
+ if (ujoins) {
+ ObjectLock olock(ujoins);
+ for (const String& ujoin : ujoins) {
+ userJoinAttrs.insert(ujoin.SubStr(0, ujoin.FindFirstOf(".")));
+ }
+ }
+
+ for (int fid = 0; fid < type->GetFieldCount(); fid++) {
+ Field field = type->GetFieldInfo(fid);
+
+ if (!(field.Attributes & FANavigation))
+ continue;
+
+ if (!allJoins && userJoinAttrs.find(field.NavigationName) == userJoinAttrs.end())
+ continue;
+
+ joinAttrs.insert(field.Name);
+ }
+
+ std::unordered_map<Type*, std::pair<bool, std::unique_ptr<Expression>>> typePermissions;
+ std::unordered_map<Object*, bool> objectAccessAllowed;
+
+ for (const ConfigObject::Ptr& obj : objs) {
+ DictionaryData result1{
+ { "name", obj->GetName() },
+ { "type", obj->GetReflectionType()->GetName() }
+ };
+
+ DictionaryData metaAttrs;
+
+ if (umetas) {
+ ObjectLock olock(umetas);
+ for (const String& meta : umetas) {
+ if (meta == "used_by") {
+ Array::Ptr used_by = new Array();
+ metaAttrs.emplace_back("used_by", used_by);
+
+ for (const Object::Ptr& pobj : DependencyGraph::GetParents((obj)))
+ {
+ ConfigObject::Ptr configObj = dynamic_pointer_cast<ConfigObject>(pobj);
+
+ if (!configObj)
+ continue;
+
+ used_by->Add(new Dictionary({
+ { "type", configObj->GetReflectionType()->GetName() },
+ { "name", configObj->GetName() }
+ }));
+ }
+ } else if (meta == "location") {
+ metaAttrs.emplace_back("location", obj->GetSourceLocation());
+ } else {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid field specified for meta: " + meta);
+ return true;
+ }
+ }
+ }
+
+ result1.emplace_back("meta", new Dictionary(std::move(metaAttrs)));
+
+ try {
+ result1.emplace_back("attrs", SerializeObjectAttrs(obj, String(), uattrs, false, false));
+ } catch (const ScriptError& ex) {
+ HttpUtility::SendJsonError(response, params, 400, ex.what());
+ return true;
+ }
+
+ DictionaryData joins;
+
+ for (const String& joinAttr : joinAttrs) {
+ Object::Ptr joinedObj;
+ int fid = type->GetFieldId(joinAttr);
+
+ if (fid < 0) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid field specified for join: " + joinAttr);
+ return true;
+ }
+
+ Field field = type->GetFieldInfo(fid);
+
+ if (!(field.Attributes & FANavigation)) {
+ HttpUtility::SendJsonError(response, params, 400, "Not a joinable field: " + joinAttr);
+ return true;
+ }
+
+ joinedObj = obj->NavigateField(fid);
+
+ if (!joinedObj)
+ continue;
+
+ Type::Ptr reflectionType = joinedObj->GetReflectionType();
+ auto it = typePermissions.find(reflectionType.get());
+ bool granted;
+
+ if (it == typePermissions.end()) {
+ String permission = "objects/query/" + reflectionType->GetName();
+
+ std::unique_ptr<Expression> permissionFilter;
+ granted = FilterUtility::HasPermission(user, permission, &permissionFilter);
+
+ it = typePermissions.insert({reflectionType.get(), std::make_pair(granted, std::move(permissionFilter))}).first;
+ }
+
+ granted = it->second.first;
+ const std::unique_ptr<Expression>& permissionFilter = it->second.second;
+
+ if (!granted) {
+ // Not authorized
+ continue;
+ }
+
+ auto relation = objectAccessAllowed.find(joinedObj.get());
+ bool accessAllowed;
+
+ if (relation == objectAccessAllowed.end()) {
+ ScriptFrame permissionFrame(false, new Namespace());
+
+ try {
+ accessAllowed = FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), joinedObj);
+ } catch (const ScriptError& err) {
+ accessAllowed = false;
+ }
+
+ objectAccessAllowed.insert({joinedObj.get(), accessAllowed});
+ } else {
+ accessAllowed = relation->second;
+ }
+
+ if (!accessAllowed) {
+ // Access denied
+ continue;
+ }
+
+ String prefix = field.NavigationName;
+
+ try {
+ joins.emplace_back(prefix, SerializeObjectAttrs(joinedObj, prefix, ujoins, true, allJoins));
+ } catch (const ScriptError& ex) {
+ HttpUtility::SendJsonError(response, params, 400, ex.what());
+ return true;
+ }
+ }
+
+ result1.emplace_back("joins", new Dictionary(std::move(joins)));
+
+ results.push_back(new Dictionary(std::move(result1)));
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/objectqueryhandler.hpp b/lib/remote/objectqueryhandler.hpp
new file mode 100644
index 0000000..691b2cf
--- /dev/null
+++ b/lib/remote/objectqueryhandler.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTQUERYHANDLER_H
+#define OBJECTQUERYHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class ObjectQueryHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ObjectQueryHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+
+private:
+ static Dictionary::Ptr SerializeObjectAttrs(const Object::Ptr& object, const String& attrPrefix,
+ const Array::Ptr& attrs, bool isJoin, bool allAttrs);
+};
+
+}
+
+#endif /* OBJECTQUERYHANDLER_H */
diff --git a/lib/remote/pkiutility.cpp b/lib/remote/pkiutility.cpp
new file mode 100644
index 0000000..00d6525
--- /dev/null
+++ b/lib/remote/pkiutility.cpp
@@ -0,0 +1,452 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/pkiutility.hpp"
+#include "remote/apilistener.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/tlsutility.hpp"
+#include "base/console.hpp"
+#include "base/tlsstream.hpp"
+#include "base/tcpsocket.hpp"
+#include "base/json.hpp"
+#include "base/utility.hpp"
+#include "base/convert.hpp"
+#include "base/exception.hpp"
+#include "remote/jsonrpc.hpp"
+#include <fstream>
+#include <iostream>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/filesystem/path.hpp>
+
+using namespace icinga;
+
+int PkiUtility::NewCa()
+{
+ String caDir = ApiListener::GetCaDir();
+ String caCertFile = caDir + "/ca.crt";
+ String caKeyFile = caDir + "/ca.key";
+
+ if (Utility::PathExists(caCertFile) && Utility::PathExists(caKeyFile)) {
+ Log(LogWarning, "cli")
+ << "CA files '" << caCertFile << "' and '" << caKeyFile << "' already exist.";
+ return 1;
+ }
+
+ Utility::MkDirP(caDir, 0700);
+
+ MakeX509CSR("Icinga CA", caKeyFile, String(), caCertFile, true);
+
+ return 0;
+}
+
+int PkiUtility::NewCert(const String& cn, const String& keyfile, const String& csrfile, const String& certfile)
+{
+ try {
+ MakeX509CSR(cn, keyfile, csrfile, certfile);
+ } catch(std::exception&) {
+ return 1;
+ }
+
+ return 0;
+}
+
+int PkiUtility::SignCsr(const String& csrfile, const String& certfile)
+{
+ char errbuf[256];
+
+ InitializeOpenSSL();
+
+ BIO *csrbio = BIO_new_file(csrfile.CStr(), "r");
+ X509_REQ *req = PEM_read_bio_X509_REQ(csrbio, nullptr, nullptr, nullptr);
+
+ if (!req) {
+ ERR_error_string_n(ERR_peek_error(), errbuf, sizeof errbuf);
+ Log(LogCritical, "SSL")
+ << "Could not read X509 certificate request from '" << csrfile << "': " << ERR_peek_error() << ", \"" << errbuf << "\"";
+ return 1;
+ }
+
+ BIO_free(csrbio);
+
+ std::shared_ptr<EVP_PKEY> pubkey = std::shared_ptr<EVP_PKEY>(X509_REQ_get_pubkey(req), EVP_PKEY_free);
+ std::shared_ptr<X509> cert = CreateCertIcingaCA(pubkey.get(), X509_REQ_get_subject_name(req));
+
+ X509_REQ_free(req);
+
+ WriteCert(cert, certfile);
+
+ return 0;
+}
+
+std::shared_ptr<X509> PkiUtility::FetchCert(const String& host, const String& port)
+{
+ Shared<boost::asio::ssl::context>::Ptr sslContext;
+
+ try {
+ sslContext = MakeAsioSslContext();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "pki")
+ << "Cannot make SSL context.";
+ Log(LogDebug, "pki")
+ << "Cannot make SSL context:\n" << DiagnosticInformation(ex);
+ return std::shared_ptr<X509>();
+ }
+
+ auto stream (Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *sslContext, host));
+
+ try {
+ Connect(stream->lowest_layer(), host, port);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "pki")
+ << "Cannot connect to host '" << host << "' on port '" << port << "'";
+ Log(LogDebug, "pki")
+ << "Cannot connect to host '" << host << "' on port '" << port << "':\n" << DiagnosticInformation(ex);
+ return std::shared_ptr<X509>();
+ }
+
+ auto& sslConn (stream->next_layer());
+
+ try {
+ sslConn.handshake(sslConn.client);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "pki")
+ << "Client TLS handshake failed. (" << ex.what() << ")";
+ return std::shared_ptr<X509>();
+ }
+
+ Defer shutdown ([&sslConn]() { sslConn.shutdown(); });
+
+ return sslConn.GetPeerCertificate();
+}
+
+int PkiUtility::WriteCert(const std::shared_ptr<X509>& cert, const String& trustedfile)
+{
+ std::ofstream fpcert;
+ fpcert.open(trustedfile.CStr());
+ fpcert << CertificateToString(cert);
+ fpcert.close();
+
+ if (fpcert.fail()) {
+ Log(LogCritical, "pki")
+ << "Could not write certificate to file '" << trustedfile << "'.";
+ return 1;
+ }
+
+ Log(LogInformation, "pki")
+ << "Writing certificate to file '" << trustedfile << "'.";
+
+ return 0;
+}
+
+int PkiUtility::GenTicket(const String& cn, const String& salt, std::ostream& ticketfp)
+{
+ ticketfp << PBKDF2_SHA1(cn, salt, 50000) << "\n";
+
+ return 0;
+}
+
+int PkiUtility::RequestCertificate(const String& host, const String& port, const String& keyfile,
+ const String& certfile, const String& cafile, const std::shared_ptr<X509>& trustedCert, const String& ticket)
+{
+ Shared<boost::asio::ssl::context>::Ptr sslContext;
+
+ try {
+ sslContext = MakeAsioSslContext(certfile, keyfile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot make SSL context for cert path: '" << certfile << "' key path: '" << keyfile << "' ca path: '" << cafile << "'.";
+ Log(LogDebug, "cli")
+ << "Cannot make SSL context for cert path: '" << certfile << "' key path: '" << keyfile << "' ca path: '" << cafile << "':\n" << DiagnosticInformation(ex);
+ return 1;
+ }
+
+ auto stream (Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *sslContext, host));
+
+ try {
+ Connect(stream->lowest_layer(), host, port);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot connect to host '" << host << "' on port '" << port << "'";
+ Log(LogDebug, "cli")
+ << "Cannot connect to host '" << host << "' on port '" << port << "':\n" << DiagnosticInformation(ex);
+ return 1;
+ }
+
+ auto& sslConn (stream->next_layer());
+
+ try {
+ sslConn.handshake(sslConn.client);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Client TLS handshake failed: " << DiagnosticInformation(ex, false);
+ return 1;
+ }
+
+ Defer shutdown ([&sslConn]() { sslConn.shutdown(); });
+
+ auto peerCert (sslConn.GetPeerCertificate());
+
+ if (X509_cmp(peerCert.get(), trustedCert.get())) {
+ Log(LogCritical, "cli", "Peer certificate does not match trusted certificate.");
+ return 1;
+ }
+
+ Dictionary::Ptr params = new Dictionary({
+ { "ticket", String(ticket) }
+ });
+
+ String msgid = Utility::NewUniqueID();
+
+ Dictionary::Ptr request = new Dictionary({
+ { "jsonrpc", "2.0" },
+ { "id", msgid },
+ { "method", "pki::RequestCertificate" },
+ { "params", params }
+ });
+
+ Dictionary::Ptr response;
+
+ try {
+ JsonRpc::SendMessage(stream, request);
+ stream->flush();
+
+ for (;;) {
+ response = JsonRpc::DecodeMessage(JsonRpc::ReadMessage(stream));
+
+ if (response && response->Contains("error")) {
+ Log(LogCritical, "cli", "Could not fetch valid response. Please check the master log (notice or debug).");
+#ifdef I2_DEBUG
+ /* we shouldn't expose master errors to the user in production environments */
+ Log(LogCritical, "cli", response->Get("error"));
+#endif /* I2_DEBUG */
+ return 1;
+ }
+
+ if (response && (response->Get("id") != msgid))
+ continue;
+
+ break;
+ }
+ } catch (...) {
+ Log(LogCritical, "cli", "Could not fetch valid response. Please check the master log.");
+ return 1;
+ }
+
+ if (!response) {
+ Log(LogCritical, "cli", "Could not fetch valid response. Please check the master log.");
+ return 1;
+ }
+
+ Dictionary::Ptr result = response->Get("result");
+
+ if (result->Contains("ca")) {
+ try {
+ StringToCertificate(result->Get("ca"));
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Could not write CA file: " << DiagnosticInformation(ex, false);
+ return 1;
+ }
+
+ Log(LogInformation, "cli")
+ << "Writing CA certificate to file '" << cafile << "'.";
+
+ std::ofstream fpca;
+ fpca.open(cafile.CStr());
+ fpca << result->Get("ca");
+ fpca.close();
+
+ if (fpca.fail()) {
+ Log(LogCritical, "cli")
+ << "Could not open CA certificate file '" << cafile << "' for writing.";
+ return 1;
+ }
+ }
+
+ if (result->Contains("error")) {
+ LogSeverity severity;
+
+ Value vstatus;
+
+ if (!result->Get("status_code", &vstatus))
+ vstatus = 1;
+
+ int status = vstatus;
+
+ if (status == 1)
+ severity = LogCritical;
+ else {
+ severity = LogInformation;
+ Log(severity, "cli", "!!!!!!");
+ }
+
+ Log(severity, "cli")
+ << "!!! " << result->Get("error");
+
+ if (status == 1)
+ return 1;
+ else {
+ Log(severity, "cli", "!!!!!!");
+ return 0;
+ }
+ }
+
+ try {
+ StringToCertificate(result->Get("cert"));
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Could not write certificate file: " << DiagnosticInformation(ex, false);
+ return 1;
+ }
+
+ Log(LogInformation, "cli")
+ << "Writing signed certificate to file '" << certfile << "'.";
+
+ std::ofstream fpcert;
+ fpcert.open(certfile.CStr());
+ fpcert << result->Get("cert");
+ fpcert.close();
+
+ if (fpcert.fail()) {
+ Log(LogCritical, "cli")
+ << "Could not write certificate to file '" << certfile << "'.";
+ return 1;
+ }
+
+ return 0;
+}
+
+String PkiUtility::GetCertificateInformation(const std::shared_ptr<X509>& cert) {
+ BIO *out = BIO_new(BIO_s_mem());
+ String pre;
+
+ pre = "\n Version: " + Convert::ToString(GetCertificateVersion(cert));
+ BIO_write(out, pre.CStr(), pre.GetLength());
+
+ pre = "\n Subject: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ X509_NAME_print_ex(out, X509_get_subject_name(cert.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
+
+ pre = "\n Issuer: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ X509_NAME_print_ex(out, X509_get_issuer_name(cert.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
+
+ pre = "\n Valid From: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ ASN1_TIME_print(out, X509_get_notBefore(cert.get()));
+
+ pre = "\n Valid Until: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ ASN1_TIME_print(out, X509_get_notAfter(cert.get()));
+
+ pre = "\n Serial: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ ASN1_INTEGER *asn1_serial = X509_get_serialNumber(cert.get());
+ for (int i = 0; i < asn1_serial->length; i++) {
+ BIO_printf(out, "%02x%c", asn1_serial->data[i], ((i + 1 == asn1_serial->length) ? '\n' : ':'));
+ }
+
+ pre = "\n Signature Algorithm: " + GetSignatureAlgorithm(cert);
+ BIO_write(out, pre.CStr(), pre.GetLength());
+
+ pre = "\n Subject Alt Names: " + GetSubjectAltNames(cert)->Join(" ");
+ BIO_write(out, pre.CStr(), pre.GetLength());
+
+ pre = "\n Fingerprint: ";
+ BIO_write(out, pre.CStr(), pre.GetLength());
+ unsigned char md[EVP_MAX_MD_SIZE];
+ unsigned int diglen;
+ X509_digest(cert.get(), EVP_sha256(), md, &diglen);
+
+ char *data;
+ long length = BIO_get_mem_data(out, &data);
+
+ std::stringstream info;
+ info << String(data, data + length);
+
+ BIO_free(out);
+
+ for (unsigned int i = 0; i < diglen; i++) {
+ info << std::setfill('0') << std::setw(2) << std::uppercase
+ << std::hex << static_cast<int>(md[i]) << ' ';
+ }
+ info << '\n';
+
+ return info.str();
+}
+
+static void CollectRequestHandler(const Dictionary::Ptr& requests, const String& requestFile)
+{
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestFile);
+
+ if (!request)
+ return;
+
+ Dictionary::Ptr result = new Dictionary();
+
+ namespace fs = boost::filesystem;
+ fs::path file(requestFile.Begin(), requestFile.End());
+ String fingerprint = file.stem().string();
+
+ String certRequestText = request->Get("cert_request");
+ result->Set("cert_request", certRequestText);
+
+ Value vcertResponseText;
+
+ if (request->Get("cert_response", &vcertResponseText)) {
+ String certResponseText = vcertResponseText;
+ result->Set("cert_response", certResponseText);
+ }
+
+ std::shared_ptr<X509> certRequest = StringToCertificate(certRequestText);
+
+/* XXX (requires OpenSSL >= 1.0.0)
+ time_t now;
+ time(&now);
+ ASN1_TIME *tm = ASN1_TIME_adj(nullptr, now, 0, 0);
+
+ int day, sec;
+ ASN1_TIME_diff(&day, &sec, tm, X509_get_notBefore(certRequest.get()));
+
+ result->Set("timestamp", static_cast<double>(now) + day * 24 * 60 * 60 + sec); */
+
+ BIO *out = BIO_new(BIO_s_mem());
+ ASN1_TIME_print(out, X509_get_notBefore(certRequest.get()));
+
+ char *data;
+ long length;
+ length = BIO_get_mem_data(out, &data);
+
+ result->Set("timestamp", String(data, data + length));
+ BIO_free(out);
+
+ out = BIO_new(BIO_s_mem());
+ X509_NAME_print_ex(out, X509_get_subject_name(certRequest.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
+
+ length = BIO_get_mem_data(out, &data);
+
+ result->Set("subject", String(data, data + length));
+ BIO_free(out);
+
+ requests->Set(fingerprint, result);
+}
+
+Dictionary::Ptr PkiUtility::GetCertificateRequests(bool removed)
+{
+ Dictionary::Ptr requests = new Dictionary();
+
+ String requestDir = ApiListener::GetCertificateRequestsDir();
+ String ext = "json";
+
+ if (removed)
+ ext = "removed";
+
+ if (Utility::PathExists(requestDir))
+ Utility::Glob(requestDir + "/*." + ext, [requests](const String& requestFile) { CollectRequestHandler(requests, requestFile); }, GlobFile);
+
+ return requests;
+}
+
diff --git a/lib/remote/pkiutility.hpp b/lib/remote/pkiutility.hpp
new file mode 100644
index 0000000..50d47e0
--- /dev/null
+++ b/lib/remote/pkiutility.hpp
@@ -0,0 +1,41 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKIUTILITY_H
+#define PKIUTILITY_H
+
+#include "remote/i2-remote.hpp"
+#include "base/exception.hpp"
+#include "base/dictionary.hpp"
+#include "base/string.hpp"
+#include <openssl/x509v3.h>
+#include <memory>
+
+namespace icinga
+{
+
+/**
+ * @ingroup remote
+ */
+class PkiUtility
+{
+public:
+ static int NewCa();
+ static int NewCert(const String& cn, const String& keyfile, const String& csrfile, const String& certfile);
+ static int SignCsr(const String& csrfile, const String& certfile);
+ static std::shared_ptr<X509> FetchCert(const String& host, const String& port);
+ static int WriteCert(const std::shared_ptr<X509>& cert, const String& trustedfile);
+ static int GenTicket(const String& cn, const String& salt, std::ostream& ticketfp);
+ static int RequestCertificate(const String& host, const String& port, const String& keyfile,
+ const String& certfile, const String& cafile, const std::shared_ptr<X509>& trustedcert,
+ const String& ticket = String());
+ static String GetCertificateInformation(const std::shared_ptr<X509>& certificate);
+ static Dictionary::Ptr GetCertificateRequests(bool removed = false);
+
+private:
+ PkiUtility();
+
+};
+
+}
+
+#endif /* PKIUTILITY_H */
diff --git a/lib/remote/statushandler.cpp b/lib/remote/statushandler.cpp
new file mode 100644
index 0000000..1f3f618
--- /dev/null
+++ b/lib/remote/statushandler.cpp
@@ -0,0 +1,120 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/statushandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/serializer.hpp"
+#include "base/statsfunction.hpp"
+#include "base/namespace.hpp"
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/status", StatusHandler);
+
+class StatusTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(StatusTargetProvider);
+
+ void FindTargets(const String& type,
+ const std::function<void (const Value&)>& addTarget) const override
+ {
+ Namespace::Ptr statsFunctions = ScriptGlobal::Get("StatsFunctions", &Empty);
+
+ if (statsFunctions) {
+ ObjectLock olock(statsFunctions);
+
+ for (const Namespace::Pair& kv : statsFunctions)
+ addTarget(GetTargetByName("Status", kv.first));
+ }
+ }
+
+ Value GetTargetByName(const String& type, const String& name) const override
+ {
+ Namespace::Ptr statsFunctions = ScriptGlobal::Get("StatsFunctions", &Empty);
+
+ if (!statsFunctions)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("No status functions are available."));
+
+ Value vfunc;
+
+ if (!statsFunctions->Get(name, &vfunc))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid status function name."));
+
+ Function::Ptr func = vfunc;
+
+ if (!func)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid status function name."));
+
+ Dictionary::Ptr status = new Dictionary();
+ Array::Ptr perfdata = new Array();
+ func->Invoke({ status, perfdata });
+
+ return new Dictionary({
+ { "name", name },
+ { "status", status },
+ { "perfdata", Serialize(perfdata, FAState) }
+ });
+ }
+
+ bool IsValidType(const String& type) const override
+ {
+ return type == "Status";
+ }
+
+ String GetPluralName(const String& type) const override
+ {
+ return "statuses";
+ }
+};
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 3)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ QueryDescription qd;
+ qd.Types.insert("Status");
+ qd.Provider = new StatusTargetProvider();
+ qd.Permission = "status/query";
+
+ params->Set("type", "Status");
+
+ if (url->GetPath().size() >= 3)
+ params->Set("status", url->GetPath()[2]);
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(objs)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
diff --git a/lib/remote/statushandler.hpp b/lib/remote/statushandler.hpp
new file mode 100644
index 0000000..c722ab3
--- /dev/null
+++ b/lib/remote/statushandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef STATUSHANDLER_H
+#define STATUSHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class StatusHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(StatusHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* STATUSHANDLER_H */
diff --git a/lib/remote/templatequeryhandler.cpp b/lib/remote/templatequeryhandler.cpp
new file mode 100644
index 0000000..e70dafb
--- /dev/null
+++ b/lib/remote/templatequeryhandler.cpp
@@ -0,0 +1,136 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/templatequeryhandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "config/configitem.hpp"
+#include "base/configtype.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/logger.hpp"
+#include <boost/algorithm/string/case_conv.hpp>
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/templates", TemplateQueryHandler);
+
+class TemplateTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TemplateTargetProvider);
+
+ static Dictionary::Ptr GetTargetForTemplate(const ConfigItem::Ptr& item)
+ {
+ DebugInfo di = item->GetDebugInfo();
+
+ return new Dictionary({
+ { "name", item->GetName() },
+ { "type", item->GetType()->GetName() },
+ { "location", new Dictionary({
+ { "path", di.Path },
+ { "first_line", di.FirstLine },
+ { "first_column", di.FirstColumn },
+ { "last_line", di.LastLine },
+ { "last_column", di.LastColumn }
+ }) }
+ });
+ }
+
+ void FindTargets(const String& type,
+ const std::function<void (const Value&)>& addTarget) const override
+ {
+ Type::Ptr ptype = Type::GetByName(type);
+
+ for (const ConfigItem::Ptr& item : ConfigItem::GetItems(ptype)) {
+ if (item->IsAbstract())
+ addTarget(GetTargetForTemplate(item));
+ }
+ }
+
+ Value GetTargetByName(const String& type, const String& name) const override
+ {
+ Type::Ptr ptype = Type::GetByName(type);
+
+ ConfigItem::Ptr item = ConfigItem::GetByTypeAndName(ptype, name);
+
+ if (!item || !item->IsAbstract())
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Template does not exist."));
+
+ return GetTargetForTemplate(item);
+ }
+
+ bool IsValidType(const String& type) const override
+ {
+ Type::Ptr ptype = Type::GetByName(type);
+
+ if (!ptype)
+ return false;
+
+ return ConfigObject::TypeInstance->IsAssignableFrom(ptype);
+ }
+
+ String GetPluralName(const String& type) const override
+ {
+ return Type::GetByName(type)->GetPluralName();
+ }
+};
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() < 3 || url->GetPath().size() > 4)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ Type::Ptr type = FilterUtility::TypeFromPluralName(url->GetPath()[2]);
+
+ if (!type) {
+ HttpUtility::SendJsonError(response, params, 400, "Invalid type specified.");
+ return true;
+ }
+
+ QueryDescription qd;
+ qd.Types.insert(type->GetName());
+ qd.Permission = "templates/query/" + type->GetName();
+ qd.Provider = new TemplateTargetProvider();
+
+ params->Set("type", type->GetName());
+
+ if (url->GetPath().size() >= 4) {
+ String attr = type->GetName();
+ boost::algorithm::to_lower(attr);
+ params->Set(attr, url->GetPath()[3]);
+ }
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user, "tmpl");
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No templates found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(objs)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/templatequeryhandler.hpp b/lib/remote/templatequeryhandler.hpp
new file mode 100644
index 0000000..503bc85
--- /dev/null
+++ b/lib/remote/templatequeryhandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TEMPLATEQUERYHANDLER_H
+#define TEMPLATEQUERYHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class TemplateQueryHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TemplateQueryHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* TEMPLATEQUERYHANDLER_H */
diff --git a/lib/remote/typequeryhandler.cpp b/lib/remote/typequeryhandler.cpp
new file mode 100644
index 0000000..4e82653
--- /dev/null
+++ b/lib/remote/typequeryhandler.cpp
@@ -0,0 +1,156 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/typequeryhandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/configtype.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/logger.hpp"
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/types", TypeQueryHandler);
+
+class TypeTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TypeTargetProvider);
+
+ void FindTargets(const String& type,
+ const std::function<void (const Value&)>& addTarget) const override
+ {
+ for (const Type::Ptr& target : Type::GetAllTypes()) {
+ addTarget(target);
+ }
+ }
+
+ Value GetTargetByName(const String& type, const String& name) const override
+ {
+ Type::Ptr ptype = Type::GetByName(name);
+
+ if (!ptype)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Type does not exist."));
+
+ return ptype;
+ }
+
+ bool IsValidType(const String& type) const override
+ {
+ return type == "Type";
+ }
+
+ String GetPluralName(const String& type) const override
+ {
+ return "types";
+ }
+};
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 3)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ QueryDescription qd;
+ qd.Types.insert("Type");
+ qd.Permission = "types";
+ qd.Provider = new TypeTargetProvider();
+
+ if (params->Contains("type"))
+ params->Set("name", params->Get("type"));
+
+ params->Set("type", "Type");
+
+ if (url->GetPath().size() >= 3)
+ params->Set("name", url->GetPath()[2]);
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No objects found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ ArrayData results;
+
+ for (const Type::Ptr& obj : objs) {
+ Dictionary::Ptr result1 = new Dictionary();
+ results.push_back(result1);
+
+ Dictionary::Ptr resultAttrs = new Dictionary();
+ result1->Set("name", obj->GetName());
+ result1->Set("plural_name", obj->GetPluralName());
+ if (obj->GetBaseType())
+ result1->Set("base", obj->GetBaseType()->GetName());
+ result1->Set("abstract", obj->IsAbstract());
+ result1->Set("fields", resultAttrs);
+
+ Dictionary::Ptr prototype = dynamic_pointer_cast<Dictionary>(obj->GetPrototype());
+ Array::Ptr prototypeKeys = new Array();
+ result1->Set("prototype_keys", prototypeKeys);
+
+ if (prototype) {
+ ObjectLock olock(prototype);
+ for (const Dictionary::Pair& kv : prototype) {
+ prototypeKeys->Add(kv.first);
+ }
+ }
+
+ int baseFieldCount = 0;
+
+ if (obj->GetBaseType())
+ baseFieldCount = obj->GetBaseType()->GetFieldCount();
+
+ for (int fid = baseFieldCount; fid < obj->GetFieldCount(); fid++) {
+ Field field = obj->GetFieldInfo(fid);
+
+ Dictionary::Ptr fieldInfo = new Dictionary();
+ resultAttrs->Set(field.Name, fieldInfo);
+
+ fieldInfo->Set("id", fid);
+ fieldInfo->Set("type", field.TypeName);
+ if (field.RefTypeName)
+ fieldInfo->Set("ref_type", field.RefTypeName);
+ if (field.Attributes & FANavigation)
+ fieldInfo->Set("navigation_name", field.NavigationName);
+ fieldInfo->Set("array_rank", field.ArrayRank);
+
+ fieldInfo->Set("attributes", new Dictionary({
+ { "config", static_cast<bool>(field.Attributes & FAConfig) },
+ { "state", static_cast<bool>(field.Attributes & FAState) },
+ { "required", static_cast<bool>(field.Attributes & FARequired) },
+ { "navigation", static_cast<bool>(field.Attributes & FANavigation) },
+ { "no_user_modify", static_cast<bool>(field.Attributes & FANoUserModify) },
+ { "no_user_view", static_cast<bool>(field.Attributes & FANoUserView) },
+ { "deprecated", static_cast<bool>(field.Attributes & FADeprecated) }
+ }));
+ }
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
diff --git a/lib/remote/typequeryhandler.hpp b/lib/remote/typequeryhandler.hpp
new file mode 100644
index 0000000..5489cb2
--- /dev/null
+++ b/lib/remote/typequeryhandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef TYPEQUERYHANDLER_H
+#define TYPEQUERYHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class TypeQueryHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(TypeQueryHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* TYPEQUERYHANDLER_H */
diff --git a/lib/remote/url-characters.hpp b/lib/remote/url-characters.hpp
new file mode 100644
index 0000000..3cc4921
--- /dev/null
+++ b/lib/remote/url-characters.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef URL_CHARACTERS_H
+#define URL_CHARACTERS_H
+
+#define ALPHA "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+#define NUMERIC "0123456789"
+
+#define UNRESERVED ALPHA NUMERIC "-._~" "%"
+#define GEN_DELIMS ":/?#[]@"
+#define SUB_DELIMS "!$&'()*+,;="
+#define PCHAR UNRESERVED SUB_DELIMS ":@"
+#define PCHAR_ENCODE UNRESERVED ":@"
+
+#define ACSCHEME ALPHA NUMERIC ".-+"
+
+//authority = [ userinfo "@" ] host [ ":" port ]
+#define ACUSERINFO UNRESERVED SUB_DELIMS
+#define ACHOST UNRESERVED SUB_DELIMS
+#define ACPORT NUMERIC
+
+#define ACPATHSEGMENT PCHAR
+#define ACPATHSEGMENT_ENCODE PCHAR_ENCODE
+#define ACQUERY PCHAR "/?"
+#define ACQUERY_ENCODE PCHAR_ENCODE "/?"
+#define ACFRAGMENT PCHAR "/?"
+#define ACFRAGMENT_ENCODE PCHAR_ENCODE "/?"
+
+#endif /* URL_CHARACTERS_H */
diff --git a/lib/remote/url.cpp b/lib/remote/url.cpp
new file mode 100644
index 0000000..e87628e
--- /dev/null
+++ b/lib/remote/url.cpp
@@ -0,0 +1,363 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/array.hpp"
+#include "base/utility.hpp"
+#include "base/objectlock.hpp"
+#include "remote/url.hpp"
+#include "remote/url-characters.hpp"
+#include <boost/tokenizer.hpp>
+
+using namespace icinga;
+
+Url::Url(const String& base_url)
+{
+ String url = base_url;
+
+ if (url.GetLength() == 0)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Empty URL."));
+
+ size_t pHelper = String::NPos;
+ if (url[0] != '/')
+ pHelper = url.Find(":");
+
+ if (pHelper != String::NPos) {
+ if (!ParseScheme(url.SubStr(0, pHelper)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Scheme."));
+ url = url.SubStr(pHelper + 1);
+ }
+
+ if (*url.Begin() != '/')
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL: '/' expected after scheme."));
+
+ if (url.GetLength() == 1) {
+ return;
+ }
+
+ if (*(url.Begin() + 1) == '/') {
+ pHelper = url.Find("/", 2);
+
+ if (pHelper == String::NPos)
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL: Missing '/' after authority."));
+
+ if (!ParseAuthority(url.SubStr(0, pHelper)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Authority"));
+
+ url = url.SubStr(pHelper);
+ }
+
+ if (*url.Begin() == '/') {
+ pHelper = url.FindFirstOf("#?");
+ if (!ParsePath(url.SubStr(1, pHelper - 1)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Path"));
+
+ if (pHelper != String::NPos)
+ url = url.SubStr(pHelper);
+ } else
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL: Missing path."));
+
+ if (*url.Begin() == '?') {
+ pHelper = url.Find("#");
+ if (!ParseQuery(url.SubStr(1, pHelper - 1)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Query"));
+
+ if (pHelper != String::NPos)
+ url = url.SubStr(pHelper);
+ }
+
+ if (*url.Begin() == '#') {
+ if (!ParseFragment(url.SubStr(1)))
+ BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid URL Fragment"));
+ }
+}
+
+String Url::GetScheme() const
+{
+ return m_Scheme;
+}
+
+String Url::GetAuthority() const
+{
+ if (m_Host.IsEmpty())
+ return "";
+
+ String auth;
+ if (!m_Username.IsEmpty()) {
+ auth = m_Username;
+ if (!m_Password.IsEmpty())
+ auth += ":" + m_Password;
+ auth += "@";
+ }
+
+ auth += m_Host;
+
+ if (!m_Port.IsEmpty())
+ auth += ":" + m_Port;
+
+ return auth;
+}
+
+String Url::GetUsername() const
+{
+ return m_Username;
+}
+
+String Url::GetPassword() const
+{
+ return m_Password;
+}
+
+String Url::GetHost() const
+{
+ return m_Host;
+}
+
+String Url::GetPort() const
+{
+ return m_Port;
+}
+
+const std::vector<String>& Url::GetPath() const
+{
+ return m_Path;
+}
+
+const std::vector<std::pair<String, String>>& Url::GetQuery() const
+{
+ return m_Query;
+}
+
+String Url::GetFragment() const
+{
+ return m_Fragment;
+}
+
+void Url::SetScheme(const String& scheme)
+{
+ m_Scheme = scheme;
+}
+
+void Url::SetUsername(const String& username)
+{
+ m_Username = username;
+}
+
+void Url::SetPassword(const String& password)
+{
+ m_Password = password;
+}
+
+void Url::SetHost(const String& host)
+{
+ m_Host = host;
+}
+
+void Url::SetPort(const String& port)
+{
+ m_Port = port;
+}
+
+void Url::SetPath(const std::vector<String>& path)
+{
+ m_Path = path;
+}
+
+void Url::SetQuery(const std::vector<std::pair<String, String>>& query)
+{
+ m_Query = query;
+}
+
+void Url::SetArrayFormatUseBrackets(bool useBrackets)
+{
+ m_ArrayFormatUseBrackets = useBrackets;
+}
+
+void Url::AddQueryElement(const String& name, const String& value)
+{
+ m_Query.emplace_back(name, value);
+}
+
+void Url::SetFragment(const String& fragment) {
+ m_Fragment = fragment;
+}
+
+String Url::Format(bool onlyPathAndQuery, bool printCredentials) const
+{
+ String url;
+
+ if (!onlyPathAndQuery) {
+ if (!m_Scheme.IsEmpty())
+ url += m_Scheme + ":";
+
+ if (printCredentials && !GetAuthority().IsEmpty())
+ url += "//" + GetAuthority();
+ else if (!GetHost().IsEmpty())
+ url += "//" + GetHost() + (!GetPort().IsEmpty() ? ":" + GetPort() : "");
+ }
+
+ if (m_Path.empty())
+ url += "/";
+ else {
+ for (const String& segment : m_Path) {
+ url += "/";
+ url += Utility::EscapeString(segment, ACPATHSEGMENT_ENCODE, false);
+ }
+ }
+
+ String param;
+ if (!m_Query.empty()) {
+ typedef std::pair<String, std::vector<String> > kv_pair;
+
+ for (const auto& kv : m_Query) {
+ String key = Utility::EscapeString(kv.first, ACQUERY_ENCODE, false);
+ if (param.IsEmpty())
+ param = "?";
+ else
+ param += "&";
+
+ param += key;
+ param += kv.second.IsEmpty() ?
+ String() : "=" + Utility::EscapeString(kv.second, ACQUERY_ENCODE, false);
+ }
+ }
+
+ url += param;
+
+ if (!m_Fragment.IsEmpty())
+ url += "#" + Utility::EscapeString(m_Fragment, ACFRAGMENT_ENCODE, false);
+
+ return url;
+}
+
+bool Url::ParseScheme(const String& scheme)
+{
+ m_Scheme = scheme;
+
+ if (scheme.FindFirstOf(ALPHA) != 0)
+ return false;
+
+ return (ValidateToken(scheme, ACSCHEME));
+}
+
+bool Url::ParseAuthority(const String& authority)
+{
+ String auth = authority.SubStr(2);
+ size_t pos = auth.Find("@");
+ if (pos != String::NPos && pos != 0) {
+ if (!Url::ParseUserinfo(auth.SubStr(0, pos)))
+ return false;
+ auth = auth.SubStr(pos+1);
+ }
+
+ pos = auth.Find(":");
+ if (pos != String::NPos) {
+ if (pos == 0 || pos == auth.GetLength() - 1 || !Url::ParsePort(auth.SubStr(pos+1)))
+ return false;
+ }
+
+ m_Host = auth.SubStr(0, pos);
+ return ValidateToken(m_Host, ACHOST);
+}
+
+bool Url::ParseUserinfo(const String& userinfo)
+{
+ size_t pos = userinfo.Find(":");
+ m_Username = userinfo.SubStr(0, pos);
+ if (!ValidateToken(m_Username, ACUSERINFO))
+ return false;
+ m_Username = Utility::UnescapeString(m_Username);
+ if (pos != String::NPos && pos != userinfo.GetLength() - 1) {
+ m_Password = userinfo.SubStr(pos+1);
+ if (!ValidateToken(m_Username, ACUSERINFO))
+ return false;
+ m_Password = Utility::UnescapeString(m_Password);
+ } else
+ m_Password = "";
+
+ return true;
+}
+
+bool Url::ParsePort(const String& port)
+{
+ m_Port = Utility::UnescapeString(port);
+ if (!ValidateToken(m_Port, ACPORT))
+ return false;
+ return true;
+}
+
+bool Url::ParsePath(const String& path)
+{
+ const std::string& pathStr = path;
+ boost::char_separator<char> sep("/");
+ boost::tokenizer<boost::char_separator<char> > tokens(pathStr, sep);
+
+ for (const String& token : tokens) {
+ if (token.IsEmpty())
+ continue;
+
+ if (!ValidateToken(token, ACPATHSEGMENT))
+ return false;
+
+ m_Path.emplace_back(Utility::UnescapeString(token));
+ }
+
+ return true;
+}
+
+bool Url::ParseQuery(const String& query)
+{
+ /* Tokenizer does not like String AT ALL */
+ const std::string& queryStr = query;
+ boost::char_separator<char> sep("&");
+ boost::tokenizer<boost::char_separator<char> > tokens(queryStr, sep);
+
+ for (const String& token : tokens) {
+ size_t pHelper = token.Find("=");
+
+ if (pHelper == 0)
+ // /?foo=bar&=bar == invalid
+ return false;
+
+ String key = token.SubStr(0, pHelper);
+ String value = Empty;
+
+ if (pHelper != String::NPos && pHelper != token.GetLength() - 1)
+ value = token.SubStr(pHelper+1);
+
+ if (!ValidateToken(value, ACQUERY))
+ return false;
+
+ value = Utility::UnescapeString(value);
+
+ pHelper = key.Find("[]");
+
+ if (pHelper == 0 || (pHelper != String::NPos && pHelper != key.GetLength()-2))
+ return false;
+
+ key = key.SubStr(0, pHelper);
+
+ if (!ValidateToken(key, ACQUERY))
+ return false;
+
+ m_Query.emplace_back(Utility::UnescapeString(key), std::move(value));
+ }
+
+ return true;
+}
+
+bool Url::ParseFragment(const String& fragment)
+{
+ m_Fragment = Utility::UnescapeString(fragment);
+
+ return ValidateToken(fragment, ACFRAGMENT);
+}
+
+bool Url::ValidateToken(const String& token, const String& symbols)
+{
+ for (const char ch : token) {
+ if (symbols.FindFirstOf(ch) == String::NPos)
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/lib/remote/url.hpp b/lib/remote/url.hpp
new file mode 100644
index 0000000..6012b2f
--- /dev/null
+++ b/lib/remote/url.hpp
@@ -0,0 +1,78 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef URL_H
+#define URL_H
+
+#include "remote/i2-remote.hpp"
+#include "base/object.hpp"
+#include "base/string.hpp"
+#include "base/array.hpp"
+#include "base/value.hpp"
+#include <map>
+#include <utility>
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * A url class to use with the API
+ *
+ * @ingroup base
+ */
+class Url final : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(Url);
+
+ Url() = default;
+ Url(const String& url);
+
+ String Format(bool onlyPathAndQuery = false, bool printCredentials = false) const;
+
+ String GetScheme() const;
+ String GetAuthority() const;
+ String GetUsername() const;
+ String GetPassword() const;
+ String GetHost() const;
+ String GetPort() const;
+ const std::vector<String>& GetPath() const;
+ const std::vector<std::pair<String, String>>& GetQuery() const;
+ String GetFragment() const;
+
+ void SetScheme(const String& scheme);
+ void SetUsername(const String& username);
+ void SetPassword(const String& password);
+ void SetHost(const String& host);
+ void SetPort(const String& port);
+ void SetPath(const std::vector<String>& path);
+ void SetQuery(const std::vector<std::pair<String, String>>& query);
+ void SetArrayFormatUseBrackets(bool useBrackets = true);
+
+ void AddQueryElement(const String& name, const String& query);
+ void SetFragment(const String& fragment);
+
+private:
+ String m_Scheme;
+ String m_Username;
+ String m_Password;
+ String m_Host;
+ String m_Port;
+ std::vector<String> m_Path;
+ std::vector<std::pair<String, String>> m_Query;
+ bool m_ArrayFormatUseBrackets;
+ String m_Fragment;
+
+ bool ParseScheme(const String& scheme);
+ bool ParseAuthority(const String& authority);
+ bool ParseUserinfo(const String& userinfo);
+ bool ParsePort(const String& port);
+ bool ParsePath(const String& path);
+ bool ParseQuery(const String& query);
+ bool ParseFragment(const String& fragment);
+
+ static bool ValidateToken(const String& token, const String& symbols);
+};
+
+}
+#endif /* URL_H */
diff --git a/lib/remote/variablequeryhandler.cpp b/lib/remote/variablequeryhandler.cpp
new file mode 100644
index 0000000..50c0e78
--- /dev/null
+++ b/lib/remote/variablequeryhandler.cpp
@@ -0,0 +1,121 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/variablequeryhandler.hpp"
+#include "remote/httputility.hpp"
+#include "remote/filterutility.hpp"
+#include "base/configtype.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/logger.hpp"
+#include "base/serializer.hpp"
+#include "base/namespace.hpp"
+#include <set>
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/variables", VariableQueryHandler);
+
+class VariableTargetProvider final : public TargetProvider
+{
+public:
+ DECLARE_PTR_TYPEDEFS(VariableTargetProvider);
+
+ static Dictionary::Ptr GetTargetForVar(const String& name, const Value& value)
+ {
+ return new Dictionary({
+ { "name", name },
+ { "type", value.GetReflectionType()->GetName() },
+ { "value", value }
+ });
+ }
+
+ void FindTargets(const String& type,
+ const std::function<void (const Value&)>& addTarget) const override
+ {
+ {
+ Namespace::Ptr globals = ScriptGlobal::GetGlobals();
+ ObjectLock olock(globals);
+ for (const Namespace::Pair& kv : globals) {
+ addTarget(GetTargetForVar(kv.first, kv.second.Val));
+ }
+ }
+ }
+
+ Value GetTargetByName(const String& type, const String& name) const override
+ {
+ return GetTargetForVar(name, ScriptGlobal::Get(name));
+ }
+
+ bool IsValidType(const String& type) const override
+ {
+ return type == "Variable";
+ }
+
+ String GetPluralName(const String& type) const override
+ {
+ return "variables";
+ }
+};
+
+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,
+ HttpServerConnection& server
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() > 3)
+ return false;
+
+ if (request.method() != http::verb::get)
+ return false;
+
+ QueryDescription qd;
+ qd.Types.insert("Variable");
+ qd.Permission = "variables";
+ qd.Provider = new VariableTargetProvider();
+
+ params->Set("type", "Variable");
+
+ if (url->GetPath().size() >= 3)
+ params->Set("variable", url->GetPath()[2]);
+
+ std::vector<Value> objs;
+
+ try {
+ objs = FilterUtility::GetFilterTargets(qd, params, user, "variable");
+ } catch (const std::exception& ex) {
+ HttpUtility::SendJsonError(response, params, 404,
+ "No variables found.",
+ DiagnosticInformation(ex));
+ return true;
+ }
+
+ ArrayData results;
+
+ for (const Dictionary::Ptr& var : objs) {
+ if (var->Get("name") == "TicketSalt")
+ continue;
+
+ results.emplace_back(new Dictionary({
+ { "name", var->Get("name") },
+ { "type", var->Get("type") },
+ { "value", Serialize(var->Get("value"), 0) }
+ }));
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array(std::move(results)) }
+ });
+
+ response.result(http::status::ok);
+ HttpUtility::SendJsonBody(response, params, result);
+
+ return true;
+}
+
diff --git a/lib/remote/variablequeryhandler.hpp b/lib/remote/variablequeryhandler.hpp
new file mode 100644
index 0000000..48e73be
--- /dev/null
+++ b/lib/remote/variablequeryhandler.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VARIABLEQUERYHANDLER_H
+#define VARIABLEQUERYHANDLER_H
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class VariableQueryHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(VariableQueryHandler);
+
+ 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,
+ HttpServerConnection& server
+ ) override;
+};
+
+}
+
+#endif /* VARIABLEQUERYHANDLER_H */
diff --git a/lib/remote/zone.cpp b/lib/remote/zone.cpp
new file mode 100644
index 0000000..5ae1468
--- /dev/null
+++ b/lib/remote/zone.cpp
@@ -0,0 +1,154 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "remote/zone.hpp"
+#include "remote/zone-ti.cpp"
+#include "remote/jsonrpcconnection.hpp"
+#include "base/array.hpp"
+#include "base/objectlock.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_TYPE(Zone);
+
+void Zone::OnAllConfigLoaded()
+{
+ ObjectImpl<Zone>::OnAllConfigLoaded();
+
+ m_Parent = Zone::GetByName(GetParentRaw());
+
+ if (m_Parent && m_Parent->IsGlobal())
+ BOOST_THROW_EXCEPTION(ScriptError("Zone '" + GetName() + "' can not have a global zone as parent.", GetDebugInfo()));
+
+ Zone::Ptr zone = m_Parent;
+ int levels = 0;
+
+ Array::Ptr endpoints = GetEndpointsRaw();
+
+ if (endpoints) {
+ ObjectLock olock(endpoints);
+ for (const String& endpoint : endpoints) {
+ Endpoint::Ptr ep = Endpoint::GetByName(endpoint);
+
+ if (ep)
+ ep->SetCachedZone(this);
+ }
+ }
+
+ while (zone) {
+ m_AllParents.push_back(zone);
+
+ zone = Zone::GetByName(zone->GetParentRaw());
+ levels++;
+
+ if (levels > 32)
+ BOOST_THROW_EXCEPTION(ScriptError("Infinite recursion detected while resolving zone graph. Check your zone hierarchy.", GetDebugInfo()));
+ }
+}
+
+Zone::Ptr Zone::GetParent() const
+{
+ return m_Parent;
+}
+
+std::set<Endpoint::Ptr> Zone::GetEndpoints() const
+{
+ std::set<Endpoint::Ptr> result;
+
+ Array::Ptr endpoints = GetEndpointsRaw();
+
+ if (endpoints) {
+ ObjectLock olock(endpoints);
+
+ for (const String& name : endpoints) {
+ Endpoint::Ptr endpoint = Endpoint::GetByName(name);
+
+ if (!endpoint)
+ continue;
+
+ result.insert(endpoint);
+ }
+ }
+
+ return result;
+}
+
+std::vector<Zone::Ptr> Zone::GetAllParentsRaw() const
+{
+ return m_AllParents;
+}
+
+Array::Ptr Zone::GetAllParents() const
+{
+ auto result (new Array);
+
+ for (auto& parent : m_AllParents)
+ result->Add(parent->GetName());
+
+ return result;
+}
+
+bool Zone::CanAccessObject(const ConfigObject::Ptr& object)
+{
+ Zone::Ptr object_zone;
+
+ if (object->GetReflectionType() == Zone::TypeInstance)
+ object_zone = static_pointer_cast<Zone>(object);
+ else
+ object_zone = static_pointer_cast<Zone>(object->GetZone());
+
+ if (!object_zone)
+ object_zone = Zone::GetLocalZone();
+
+ if (object_zone->GetGlobal())
+ return true;
+
+ return object_zone->IsChildOf(this);
+}
+
+bool Zone::IsChildOf(const Zone::Ptr& zone)
+{
+ Zone::Ptr azone = this;
+
+ while (azone) {
+ if (azone == zone)
+ return true;
+
+ azone = azone->GetParent();
+ }
+
+ return false;
+}
+
+bool Zone::IsGlobal() const
+{
+ return GetGlobal();
+}
+
+bool Zone::IsSingleInstance() const
+{
+ Array::Ptr endpoints = GetEndpointsRaw();
+ return !endpoints || endpoints->GetLength() < 2;
+}
+
+Zone::Ptr Zone::GetLocalZone()
+{
+ Endpoint::Ptr local = Endpoint::GetLocalEndpoint();
+
+ if (!local)
+ return nullptr;
+
+ return local->GetZone();
+}
+
+void Zone::ValidateEndpointsRaw(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils)
+{
+ ObjectImpl<Zone>::ValidateEndpointsRaw(lvalue, utils);
+
+ if (lvalue() && lvalue()->GetLength() > 2) {
+ Log(LogWarning, "Zone")
+ << "The Zone object '" << GetName() << "' has more than two endpoints."
+ << " Due to a known issue this type of configuration is strongly"
+ << " discouraged and may cause Icinga to use excessive amounts of CPU time.";
+ }
+}
diff --git a/lib/remote/zone.hpp b/lib/remote/zone.hpp
new file mode 100644
index 0000000..897b18e
--- /dev/null
+++ b/lib/remote/zone.hpp
@@ -0,0 +1,46 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef ZONE_H
+#define ZONE_H
+
+#include "remote/i2-remote.hpp"
+#include "remote/zone-ti.hpp"
+#include "remote/endpoint.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup remote
+ */
+class Zone final : public ObjectImpl<Zone>
+{
+public:
+ DECLARE_OBJECT(Zone);
+ DECLARE_OBJECTNAME(Zone);
+
+ void OnAllConfigLoaded() override;
+
+ Zone::Ptr GetParent() const;
+ std::set<Endpoint::Ptr> GetEndpoints() const;
+ std::vector<Zone::Ptr> GetAllParentsRaw() const;
+ Array::Ptr GetAllParents() const override;
+
+ bool CanAccessObject(const ConfigObject::Ptr& object);
+ bool IsChildOf(const Zone::Ptr& zone);
+ bool IsGlobal() const;
+ bool IsSingleInstance() const;
+
+ static Zone::Ptr GetLocalZone();
+
+protected:
+ void ValidateEndpointsRaw(const Lazy<Array::Ptr>& lvalue, const ValidationUtils& utils) override;
+
+private:
+ Zone::Ptr m_Parent;
+ std::vector<Zone::Ptr> m_AllParents;
+};
+
+}
+
+#endif /* ZONE_H */
diff --git a/lib/remote/zone.ti b/lib/remote/zone.ti
new file mode 100644
index 0000000..25f6a64
--- /dev/null
+++ b/lib/remote/zone.ti
@@ -0,0 +1,25 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "base/configobject.hpp"
+
+library remote;
+
+namespace icinga
+{
+
+class Zone : ConfigObject
+{
+ [config, no_user_modify, navigation] name(Zone) parent (ParentRaw) {
+ navigate {{{
+ return Zone::GetByName(GetParentRaw());
+ }}}
+ };
+
+ [config] array(name(Endpoint)) endpoints (EndpointsRaw);
+ [config] bool global;
+ [no_user_modify, no_storage] array(Value) all_parents {
+ get;
+ };
+};
+
+}