icinga2/lib/remote/consolehandler.cpp
Julian Brost b671e80050 /v1/console: prevent concurrent use of the same session by multiple requests
If there are such requests, without this change, they would all be allowed and
processed, resulting in unsafe concurrent (write) access to these data
structures, which can ultimately crash the daemon or lead to other unintended
behavior.
2026-01-07 12:43:16 +01:00

352 lines
8.9 KiB
C++

/* 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/application.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/utility.hpp"
#include <boost/thread/once.hpp>
#include <memory>
using namespace icinga;
REGISTER_URLHANDLER("/v1/console", ConsoleHandler);
static std::map<String, std::shared_ptr<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;
for (auto& kv : l_ApiScriptFrames) {
std::unique_lock frameLock(kv.second->Mutex, std::try_to_lock);
if (!frameLock) {
// If the frame is locked, it's in use, don't expire it this time.
continue;
}
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();
});
}
static std::shared_ptr<ApiScriptFrame> GetOrCreateScriptFrame(const String& session) {
std::unique_lock<std::mutex> lock(l_ApiScriptMutex);
auto& frame = l_ApiScriptFrames[session];
// If no session was found, create a new one
if (!frame) {
frame = std::make_shared<ApiScriptFrame>();
}
return frame;
}
bool ConsoleHandler::HandleRequest(
const WaitGroup::Ptr&,
const HttpRequest& request,
HttpResponse& response,
boost::asio::yield_context& yc
)
{
namespace http = boost::beast::http;
auto url = request.Url();
auto user = request.User();
auto params = request.Params();
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, command, session, sandboxed);
else if (methodName == "auto-complete-script")
return AutocompleteScriptHelper(request, response, command, session, sandboxed);
HttpUtility::SendJsonError(response, params, 400, "Invalid method specified: " + methodName);
return true;
}
bool ConsoleHandler::ExecuteScriptHelper(const HttpRequest& request, HttpResponse& response,
const String& command, const String& session, bool sandboxed)
{
namespace http = boost::beast::http;
Log(LogNotice, "Console")
<< "Executing expression: " << command;
EnsureFrameCleanupTimer();
auto lsf = GetOrCreateScriptFrame(session);
std::unique_lock frameLock(lsf->Mutex, std::try_to_lock);
if (!frameLock) {
HttpUtility::SendJsonError(response, request.Params(), 409, "Session is currently in use by another request.");
return true;
}
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, request.Params(), result);
return true;
}
bool ConsoleHandler::AutocompleteScriptHelper(const HttpRequest& request, HttpResponse& response,
const String& command, const String& session, bool sandboxed)
{
namespace http = boost::beast::http;
Log(LogInformation, "Console")
<< "Auto-completing expression: " << command;
EnsureFrameCleanupTimer();
auto lsf = GetOrCreateScriptFrame(session);
std::unique_lock frameLock(lsf->Mutex, std::try_to_lock);
if (!frameLock) {
HttpUtility::SendJsonError(response, request.Params(), 409, "Session is currently in use by another request.");
return true;
}
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, request.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;
}