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