1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2 
3 #include "remote/consolehandler.hpp"
4 #include "remote/httputility.hpp"
5 #include "remote/filterutility.hpp"
6 #include "config/configcompiler.hpp"
7 #include "base/configtype.hpp"
8 #include "base/configwriter.hpp"
9 #include "base/scriptglobal.hpp"
10 #include "base/logger.hpp"
11 #include "base/serializer.hpp"
12 #include "base/timer.hpp"
13 #include "base/namespace.hpp"
14 #include "base/initialize.hpp"
15 #include "base/utility.hpp"
16 #include <boost/thread/once.hpp>
17 #include <set>
18 
19 using namespace icinga;
20 
21 REGISTER_URLHANDLER("/v1/console", ConsoleHandler);
22 
23 static std::mutex l_QueryMutex;
24 static std::map<String, ApiScriptFrame> l_ApiScriptFrames;
25 static Timer::Ptr l_FrameCleanupTimer;
26 static std::mutex l_ApiScriptMutex;
27 
ScriptFrameCleanupHandler()28 static void ScriptFrameCleanupHandler()
29 {
30 	std::unique_lock<std::mutex> lock(l_ApiScriptMutex);
31 
32 	std::vector<String> cleanup_keys;
33 
34 	typedef std::pair<String, ApiScriptFrame> KVPair;
35 
36 	for (const KVPair& kv : l_ApiScriptFrames) {
37 		if (kv.second.Seen < Utility::GetTime() - 1800)
38 			cleanup_keys.push_back(kv.first);
39 	}
40 
41 	for (const String& key : cleanup_keys)
42 		l_ApiScriptFrames.erase(key);
43 }
44 
EnsureFrameCleanupTimer()45 static void EnsureFrameCleanupTimer()
46 {
47 	static boost::once_flag once = BOOST_ONCE_INIT;
48 
49 	boost::call_once(once, []() {
50 		l_FrameCleanupTimer = new Timer();
51 		l_FrameCleanupTimer->OnTimerExpired.connect([](const Timer * const&) { ScriptFrameCleanupHandler(); });
52 		l_FrameCleanupTimer->SetInterval(30);
53 		l_FrameCleanupTimer->Start();
54 	});
55 }
56 
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)57 bool ConsoleHandler::HandleRequest(
58 	AsioTlsStream& stream,
59 	const ApiUser::Ptr& user,
60 	boost::beast::http::request<boost::beast::http::string_body>& request,
61 	const Url::Ptr& url,
62 	boost::beast::http::response<boost::beast::http::string_body>& response,
63 	const Dictionary::Ptr& params,
64 	boost::asio::yield_context& yc,
65 	HttpServerConnection& server
66 )
67 {
68 	namespace http = boost::beast::http;
69 
70 	if (url->GetPath().size() != 3)
71 		return false;
72 
73 	if (request.method() != http::verb::post)
74 		return false;
75 
76 	QueryDescription qd;
77 
78 	String methodName = url->GetPath()[2];
79 
80 	FilterUtility::CheckPermission(user, "console");
81 
82 	String session = HttpUtility::GetLastParameter(params, "session");
83 
84 	if (session.IsEmpty())
85 		session = Utility::NewUniqueID();
86 
87 	String command = HttpUtility::GetLastParameter(params, "command");
88 
89 	bool sandboxed = HttpUtility::GetLastParameter(params, "sandboxed");
90 
91 	if (methodName == "execute-script")
92 		return ExecuteScriptHelper(request, response, params, command, session, sandboxed);
93 	else if (methodName == "auto-complete-script")
94 		return AutocompleteScriptHelper(request, response, params, command, session, sandboxed);
95 
96 	HttpUtility::SendJsonError(response, params, 400, "Invalid method specified: " + methodName);
97 	return true;
98 }
99 
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)100 bool ConsoleHandler::ExecuteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
101 	boost::beast::http::response<boost::beast::http::string_body>& response,
102 	const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
103 {
104 	namespace http = boost::beast::http;
105 
106 	Log(LogNotice, "Console")
107 		<< "Executing expression: " << command;
108 
109 	EnsureFrameCleanupTimer();
110 
111 	ApiScriptFrame& lsf = l_ApiScriptFrames[session];
112 	lsf.Seen = Utility::GetTime();
113 
114 	if (!lsf.Locals)
115 		lsf.Locals = new Dictionary();
116 
117 	String fileName = "<" + Convert::ToString(lsf.NextLine) + ">";
118 	lsf.NextLine++;
119 
120 	lsf.Lines[fileName] = command;
121 
122 	Dictionary::Ptr resultInfo;
123 	std::unique_ptr<Expression> expr;
124 	Value exprResult;
125 
126 	try {
127 		expr = ConfigCompiler::CompileText(fileName, command);
128 
129 		ScriptFrame frame(true);
130 		frame.Locals = lsf.Locals;
131 		frame.Self = lsf.Locals;
132 		frame.Sandboxed = sandboxed;
133 
134 		exprResult = expr->Evaluate(frame);
135 
136 		resultInfo = new Dictionary({
137 			{ "code", 200 },
138 			{ "status", "Executed successfully." },
139 			{ "result", Serialize(exprResult, 0) }
140 		});
141 	} catch (const ScriptError& ex) {
142 		DebugInfo di = ex.GetDebugInfo();
143 
144 		std::ostringstream msgbuf;
145 
146 		msgbuf << di.Path << ": " << lsf.Lines[di.Path] << "\n"
147 			<< String(di.Path.GetLength() + 2, ' ')
148 			<< String(di.FirstColumn, ' ') << String(di.LastColumn - di.FirstColumn + 1, '^') << "\n"
149 			<< ex.what() << "\n";
150 
151 		resultInfo = new Dictionary({
152 			{ "code", 500 },
153 			{ "status", String(msgbuf.str()) },
154 			{ "incomplete_expression", ex.IsIncompleteExpression() },
155 			{ "debug_info", new Dictionary({
156 				{ "path", di.Path },
157 				{ "first_line", di.FirstLine },
158 				{ "first_column", di.FirstColumn },
159 				{ "last_line", di.LastLine },
160 				{ "last_column", di.LastColumn }
161 			}) }
162 		});
163 	}
164 
165 	Dictionary::Ptr result = new Dictionary({
166 		{ "results", new Array({ resultInfo }) }
167 	});
168 
169 	response.result(http::status::ok);
170 	HttpUtility::SendJsonBody(response, params, result);
171 
172 	return true;
173 }
174 
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)175 bool ConsoleHandler::AutocompleteScriptHelper(boost::beast::http::request<boost::beast::http::string_body>& request,
176 	boost::beast::http::response<boost::beast::http::string_body>& response,
177 	const Dictionary::Ptr& params, const String& command, const String& session, bool sandboxed)
178 {
179 	namespace http = boost::beast::http;
180 
181 	Log(LogInformation, "Console")
182 		<< "Auto-completing expression: " << command;
183 
184 	EnsureFrameCleanupTimer();
185 
186 	ApiScriptFrame& lsf = l_ApiScriptFrames[session];
187 	lsf.Seen = Utility::GetTime();
188 
189 	if (!lsf.Locals)
190 		lsf.Locals = new Dictionary();
191 
192 
193 	ScriptFrame frame(true);
194 	frame.Locals = lsf.Locals;
195 	frame.Self = lsf.Locals;
196 	frame.Sandboxed = sandboxed;
197 
198 	Dictionary::Ptr result1 = new Dictionary({
199 		{ "code", 200 },
200 		{ "status", "Auto-completed successfully." },
201 		{ "suggestions", Array::FromVector(GetAutocompletionSuggestions(command, frame)) }
202 	});
203 
204 	Dictionary::Ptr result = new Dictionary({
205 		{ "results", new Array({ result1 }) }
206 	});
207 
208 	response.result(http::status::ok);
209 	HttpUtility::SendJsonBody(response, params, result);
210 
211 	return true;
212 }
213 
AddSuggestion(std::vector<String> & matches,const String & word,const String & suggestion)214 static void AddSuggestion(std::vector<String>& matches, const String& word, const String& suggestion)
215 {
216 	if (suggestion.Find(word) != 0)
217 		return;
218 
219 	matches.push_back(suggestion);
220 }
221 
AddSuggestions(std::vector<String> & matches,const String & word,const String & pword,bool withFields,const Value & value)222 static void AddSuggestions(std::vector<String>& matches, const String& word, const String& pword, bool withFields, const Value& value)
223 {
224 	String prefix;
225 
226 	if (!pword.IsEmpty())
227 		prefix = pword + ".";
228 
229 	if (value.IsObjectType<Dictionary>()) {
230 		Dictionary::Ptr dict = value;
231 
232 		ObjectLock olock(dict);
233 		for (const Dictionary::Pair& kv : dict) {
234 			AddSuggestion(matches, word, prefix + kv.first);
235 		}
236 	}
237 
238 	if (value.IsObjectType<Namespace>()) {
239 		Namespace::Ptr ns = value;
240 
241 		ObjectLock olock(ns);
242 		for (const Namespace::Pair& kv : ns) {
243 			AddSuggestion(matches, word, prefix + kv.first);
244 		}
245 	}
246 
247 	if (withFields) {
248 		Type::Ptr type = value.GetReflectionType();
249 
250 		for (int i = 0; i < type->GetFieldCount(); i++) {
251 			Field field = type->GetFieldInfo(i);
252 
253 			AddSuggestion(matches, word, prefix + field.Name);
254 		}
255 
256 		while (type) {
257 			Object::Ptr prototype = type->GetPrototype();
258 			Dictionary::Ptr dict = dynamic_pointer_cast<Dictionary>(prototype);
259 
260 			if (dict) {
261 				ObjectLock olock(dict);
262 				for (const Dictionary::Pair& kv : dict) {
263 					AddSuggestion(matches, word, prefix + kv.first);
264 				}
265 			}
266 
267 			type = type->GetBaseType();
268 		}
269 	}
270 }
271 
GetAutocompletionSuggestions(const String & word,ScriptFrame & frame)272 std::vector<String> ConsoleHandler::GetAutocompletionSuggestions(const String& word, ScriptFrame& frame)
273 {
274 	std::vector<String> matches;
275 
276 	for (const String& keyword : ConfigWriter::GetKeywords()) {
277 		AddSuggestion(matches, word, keyword);
278 	}
279 
280 	{
281 		ObjectLock olock(frame.Locals);
282 		for (const Dictionary::Pair& kv : frame.Locals) {
283 			AddSuggestion(matches, word, kv.first);
284 		}
285 	}
286 
287 	{
288 		ObjectLock olock(ScriptGlobal::GetGlobals());
289 		for (const Namespace::Pair& kv : ScriptGlobal::GetGlobals()) {
290 			AddSuggestion(matches, word, kv.first);
291 		}
292 	}
293 
294 	Namespace::Ptr systemNS = ScriptGlobal::Get("System");
295 
296 	AddSuggestions(matches, word, "", false, systemNS);
297 	AddSuggestions(matches, word, "", true, systemNS->Get("Configuration"));
298 	AddSuggestions(matches, word, "", false, ScriptGlobal::Get("Types"));
299 	AddSuggestions(matches, word, "", false, ScriptGlobal::Get("Icinga"));
300 
301 	String::SizeType cperiod = word.RFind(".");
302 
303 	if (cperiod != String::NPos) {
304 		String pword = word.SubStr(0, cperiod);
305 
306 		Value value;
307 
308 		try {
309 			std::unique_ptr<Expression> expr = ConfigCompiler::CompileText("temp", pword);
310 
311 			if (expr)
312 				value = expr->Evaluate(frame);
313 
314 			AddSuggestions(matches, word, pword, true, value);
315 		} catch (...) { /* Ignore the exception */ }
316 	}
317 
318 	return matches;
319 }
320