/* 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 #include using namespace icinga; REGISTER_URLHANDLER("/v1/console", ConsoleHandler); static std::mutex l_QueryMutex; static std::map l_ApiScriptFrames; static Timer::Ptr l_FrameCleanupTimer; static std::mutex l_ApiScriptMutex; static void ScriptFrameCleanupHandler() { std::unique_lock lock(l_ApiScriptMutex); std::vector cleanup_keys; typedef std::pair 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& request, const Url::Ptr& url, boost::beast::http::response& 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& request, boost::beast::http::response& response, const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed) { namespace http = boost::beast::http; Log(LogNotice, "Console") << "Executing expression: " << command; 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 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& request, boost::beast::http::response& response, const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed) { namespace http = boost::beast::http; Log(LogInformation, "Console") << "Auto-completing expression: " << command; 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& matches, const String& word, const String& suggestion) { if (suggestion.Find(word) != 0) return; matches.push_back(suggestion); } static void AddSuggestions(std::vector& matches, const String& word, const String& pword, bool withFields, const Value& value) { String prefix; if (!pword.IsEmpty()) prefix = pword + "."; if (value.IsObjectType()) { Dictionary::Ptr dict = value; ObjectLock olock(dict); for (const Dictionary::Pair& kv : dict) { AddSuggestion(matches, word, prefix + kv.first); } } if (value.IsObjectType()) { 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(prototype); if (dict) { ObjectLock olock(dict); for (const Dictionary::Pair& kv : dict) { AddSuggestion(matches, word, prefix + kv.first); } } type = type->GetBaseType(); } } } std::vector ConsoleHandler::GetAutocompletionSuggestions(const String& word, ScriptFrame& frame) { std::vector 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 expr = ConfigCompiler::CompileText("temp", pword); if (expr) value = expr->Evaluate(frame); AddSuggestions(matches, word, pword, true, value); } catch (...) { /* Ignore the exception */ } } return matches; }