/** * Executes the auto completion script via HTTP and returns HTTP and user errors. * * @param session Local session handler. * @param command The auto completion string. * @param sandboxed Whether to run this sandboxed. * @return Result value, also contains user errors. */ Array::Ptr ConsoleCommand::AutoCompleteScript(const String& session, const String& command, bool sandboxed) { /* Extend the url parameters for the request. */ l_Url->SetPath({ "v1", "console", "auto-complete-script" }); l_Url->SetQuery({ {"session", session}, {"command", command}, {"sandboxed", sandboxed ? "1" : "0"} }); Dictionary::Ptr jsonResponse = SendRequest(); /* Extract the result, and handle user input errors too. */ Array::Ptr results = jsonResponse->Get("results"); Array::Ptr suggestions; if (results && results->GetLength() > 0) { Dictionary::Ptr resultInfo = results->Get(0); if (resultInfo->Get("code") >= 200 && resultInfo->Get("code") <= 299) { suggestions = resultInfo->Get("suggestions"); } else { String errorMessage = resultInfo->Get("status"); BOOST_THROW_EXCEPTION(ScriptError(errorMessage)); } } return suggestions; }
virtual bool OnInit(void) override { Application::InitializeBase(); Url::Ptr pUrl; if (argc < 2) { wxConfig config("IcingaStudio"); wxString wUrl; if (!config.Read("url", &wUrl)) wUrl = "https://localhost:5665/"; std::string url = wUrl.ToStdString(); ConnectForm f(NULL, new Url(url)); if (f.ShowModal() != wxID_OK) return false; pUrl = f.GetUrl(); url = pUrl->Format(true); wUrl = url; config.Write("url", wUrl); } else { pUrl = new Url(argv[1].ToStdString()); } MainForm *m = new MainForm(NULL, pUrl); m->Show(); return true; }
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; }
ConnectForm::ConnectForm(wxWindow *parent, const Url::Ptr& url) : ConnectFormBase(parent) { #ifdef _WIN32 SetIcon(wxICON(icinga)); #endif /* _WIN32 */ std::string authority = url->GetAuthority(); std::vector<std::string> tokens; boost::algorithm::split(tokens, authority, boost::is_any_of("@")); if (tokens.size() > 1) { std::vector<std::string> userinfo; boost::algorithm::split(userinfo, tokens[0], boost::is_any_of(":")); m_UserText->SetValue(userinfo[0]); m_PasswordText->SetValue(userinfo[1]); } std::vector<std::string> hostport; boost::algorithm::split(hostport, tokens.size() > 1 ? tokens[1] : tokens[0], boost::is_any_of(":")); m_HostText->SetValue(hostport[0]); if (hostport.size() > 1) m_PortText->SetValue(hostport[1]); else m_PortText->SetValue("5665"); SetDefaultItem(m_ButtonsOK); }
/** * Executes the DSL script via HTTP and returns HTTP and user errors. * * @param session Local session handler. * @param command The DSL string. * @param sandboxed Whether to run this sandboxed. * @return Result value, also contains user errors. */ Value ConsoleCommand::ExecuteScript(const String& session, const String& command, bool sandboxed) { /* Extend the url parameters for the request. */ l_Url->SetPath({"v1", "console", "execute-script"}); l_Url->SetQuery({ {"session", session}, {"command", command}, {"sandboxed", sandboxed ? "1" : "0"} }); Dictionary::Ptr jsonResponse = SendRequest(); /* Extract the result, and handle user input errors too. */ Array::Ptr results = jsonResponse->Get("results"); Value result; if (results && results->GetLength() > 0) { Dictionary::Ptr resultInfo = results->Get(0); if (resultInfo->Get("code") >= 200 && resultInfo->Get("code") <= 299) { result = resultInfo->Get("result"); } else { String errorMessage = resultInfo->Get("status"); DebugInfo di; Dictionary::Ptr debugInfo = resultInfo->Get("debug_info"); if (debugInfo) { di.Path = debugInfo->Get("path"); di.FirstLine = debugInfo->Get("first_line"); di.FirstColumn = debugInfo->Get("first_column"); di.LastLine = debugInfo->Get("last_line"); di.LastColumn = debugInfo->Get("last_column"); } bool incompleteExpression = resultInfo->Get("incomplete_expression"); BOOST_THROW_EXCEPTION(ScriptError(errorMessage, di, incompleteExpression)); } } return result; }
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"); 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; }
void ApiClient::ExecuteScript(const String& session, const String& command, bool sandboxed, const ExecuteScriptCompletionCallback& callback) const { Url::Ptr url = new Url(); url->SetScheme("https"); url->SetHost(m_Connection->GetHost()); url->SetPort(m_Connection->GetPort()); url->SetPath({ "v1", "console", "execute-script" }); std::map<String, std::vector<String> > params; params["session"].push_back(session); params["command"].push_back(command); params["sandboxed"].emplace_back(sandboxed ? "1" : "0"); url->SetQuery(params); try { std::shared_ptr<HttpRequest> req = m_Connection->NewRequest(); req->RequestMethod = "POST"; req->RequestUrl = url; req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password)); req->AddHeader("Accept", "application/json"); m_Connection->SubmitRequest(req, std::bind(ExecuteScriptHttpCompletionCallback, _1, _2, callback)); } catch (const std::exception&) { callback(boost::current_exception(), Empty); } }
MainForm::MainForm(wxWindow *parent, const Url::Ptr& url) : MainFormBase(parent) { #ifdef _WIN32 SetIcon(wxICON(icinga)); SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); #endif /* _WIN32 */ String port = url->GetPort(); if (port.IsEmpty()) port = "5665"; m_ApiClient = new ApiClient(url->GetHost(), port, url->GetUsername(), url->GetPassword()); m_ApiClient->GetTypes(boost::bind(&MainForm::TypesCompletionHandler, this, _1, _2, true)); std::string title = url->Format() + " - Icinga Studio"; SetTitle(title); m_ObjectsList->InsertColumn(0, "Name", 0, 300); m_PropertyGrid->SetColumnCount(3); }
void ApiClient::GetTypes(const TypesCompletionCallback& callback) const { Url::Ptr url = new Url(); url->SetScheme("https"); url->SetHost(m_Connection->GetHost()); url->SetPort(m_Connection->GetPort()); std::vector<String> path; path.push_back("v1"); path.push_back("types"); url->SetPath(path); try { boost::shared_ptr<HttpRequest> req = m_Connection->NewRequest(); req->RequestMethod = "GET"; req->RequestUrl = url; req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password)); req->AddHeader("Accept", "application/json"); m_Connection->SubmitRequest(req, boost::bind(TypesHttpCompletionCallback, _1, _2, callback)); } catch (const std::exception& ex) { callback(boost::current_exception(), std::vector<ApiType::Ptr>()); } }
/** * Connects to host:port and performs a TLS shandshake * * @returns AsioTlsStream pointer for future HTTP connections. */ std::shared_ptr<AsioTlsStream> ConsoleCommand::Connect() { std::shared_ptr<boost::asio::ssl::context> sslContext; try { sslContext = MakeAsioSslContext(Empty, Empty, Empty); //TODO: Add support for cert, key, ca parameters } catch(const std::exception& ex) { Log(LogCritical, "DebugConsole") << "Cannot make SSL context: " << ex.what(); throw; } String host = l_Url->GetHost(); String port = l_Url->GetPort(); std::shared_ptr<AsioTlsStream> stream = std::make_shared<AsioTlsStream>(IoEngine::Get().GetIoService(), *sslContext, host); try { icinga::Connect(stream->lowest_layer(), host, port); } catch (const std::exception& ex) { Log(LogWarning, "DebugConsole") << "Cannot connect to REST API on host '" << host << "' port '" << port << "': " << ex.what(); throw; } auto& tlsStream (stream->next_layer()); try { tlsStream.handshake(tlsStream.client); } catch (const std::exception& ex) { Log(LogWarning, "DebugConsole") << "TLS handshake with host '" << host << "' failed: " << ex.what(); throw; } return std::move(stream); }
void ApiClient::GetObjects(const String& pluralType, const ObjectsCompletionCallback& callback, const std::vector<String>& names, const std::vector<String>& attrs, const std::vector<String>& joins, bool all_joins) const { Url::Ptr url = new Url(); url->SetScheme("https"); url->SetHost(m_Connection->GetHost()); url->SetPort(m_Connection->GetPort()); std::vector<String> path; path.push_back("v1"); path.push_back("objects"); path.push_back(pluralType); url->SetPath(path); std::map<String, std::vector<String> > params; for (const String& name : names) { params[pluralType.ToLower()].push_back(name); } for (const String& attr : attrs) { params["attrs"].push_back(attr); } for (const String& join : joins) { params["joins"].push_back(join); } params["all_joins"].push_back(all_joins ? "1" : "0"); url->SetQuery(params); try { boost::shared_ptr<HttpRequest> req = m_Connection->NewRequest(); req->RequestMethod = "GET"; req->RequestUrl = url; req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password)); req->AddHeader("Accept", "application/json"); m_Connection->SubmitRequest(req, boost::bind(ObjectsHttpCompletionCallback, _1, _2, callback)); } catch (const std::exception& ex) { callback(boost::current_exception(), std::vector<ApiObject::Ptr>()); } }
void ElasticsearchWriter::SendRequest(const String& body) { namespace beast = boost::beast; namespace http = beast::http; Url::Ptr url = new Url(); url->SetScheme(GetEnableTls() ? "https" : "http"); url->SetHost(GetHost()); url->SetPort(GetPort()); std::vector<String> path; /* Specify the index path. Best practice is a daily rotation. * Example: http://localhost:9200/icinga2-2017.09.11?pretty=1 */ path.emplace_back(GetIndex() + "-" + Utility::FormatDateTime("%Y.%m.%d", Utility::GetTime())); /* ES 6 removes multiple _type mappings: https://www.elastic.co/guide/en/elasticsearch/reference/6.x/removal-of-types.html * Best practice is to statically define 'doc', as ES 5.X does not allow types starting with '_'. */ path.emplace_back("doc"); /* Use the bulk message format. */ path.emplace_back("_bulk"); url->SetPath(path); OptionalTlsStream stream; try { stream = Connect(); } catch (const std::exception& ex) { Log(LogWarning, "ElasticsearchWriter") << "Flush failed, cannot connect to Elasticsearch: " << DiagnosticInformation(ex, false); return; } Defer s ([&stream]() { if (stream.first) { stream.first->next_layer().shutdown(); } }); http::request<http::string_body> request (http::verb::post, std::string(url->Format(true)), 10); request.set(http::field::user_agent, "Icinga/" + Application::GetAppVersion()); request.set(http::field::host, url->GetHost() + ":" + url->GetPort()); /* Specify required headers by Elasticsearch. */ request.set(http::field::accept, "application/json"); /* Use application/x-ndjson for bulk streams. While ES * is able to handle application/json, the newline separator * causes problems with Logstash (#6609). */ request.set(http::field::content_type, "application/x-ndjson"); /* Send authentication if configured. */ String username = GetUsername(); String password = GetPassword(); if (!username.IsEmpty() && !password.IsEmpty()) request.set(http::field::authorization, "Basic " + Base64::Encode(username + ":" + password)); request.body() = body; request.set(http::field::content_length, request.body().size()); /* Don't log the request body to debug log, this is already done above. */ Log(LogDebug, "ElasticsearchWriter") << "Sending " << request.method_string() << " request" << ((!username.IsEmpty() && !password.IsEmpty()) ? " with basic auth" : "" ) << " to '" << url->Format() << "'."; try { if (stream.first) { http::write(*stream.first, request); stream.first->flush(); } else { http::write(*stream.second, request); stream.second->flush(); } } catch (const std::exception&) { Log(LogWarning, "ElasticsearchWriter") << "Cannot write to HTTP API on host '" << GetHost() << "' port '" << GetPort() << "'."; throw; } http::parser<false, http::string_body> parser; beast::flat_buffer buf; try { if (stream.first) { http::read(*stream.first, buf, parser); } else { http::read(*stream.second, buf, parser); } } catch (const std::exception& ex) { Log(LogWarning, "ElasticsearchWriter") << "Failed to parse HTTP response from host '" << GetHost() << "' port '" << GetPort() << "': " << DiagnosticInformation(ex, false); throw; } auto& response (parser.get()); if (response.result_int() > 299) { if (response.result() == http::status::unauthorized) { /* More verbose error logging with Elasticsearch is hidden behind a proxy. */ if (!username.IsEmpty() && !password.IsEmpty()) { Log(LogCritical, "ElasticsearchWriter") << "401 Unauthorized. Please ensure that the user '" << username << "' is able to authenticate against the HTTP API/Proxy."; } else { Log(LogCritical, "ElasticsearchWriter") << "401 Unauthorized. The HTTP API requires authentication but no username/password has been configured."; } return; } std::ostringstream msgbuf; msgbuf << "Unexpected response code " << response.result_int() << " from URL '" << url->Format() << "'"; auto& contentType (response[http::field::content_type]); if (contentType != "application/json" && contentType != "application/json; charset=utf-8") { msgbuf << "; Unexpected Content-Type: '" << contentType << "'"; } auto& body (response.body()); #ifdef I2_DEBUG msgbuf << "; Response body: '" << body << "'"; #endif /* I2_DEBUG */ Dictionary::Ptr jsonResponse; try { jsonResponse = JsonDecode(body); } catch (...) { Log(LogWarning, "ElasticsearchWriter") << "Unable to parse JSON response:\n" << body; return; } String error = jsonResponse->Get("error"); Log(LogCritical, "ElasticsearchWriter") << "Error: '" << error << "'. " << msgbuf.str(); } }
void InfluxdbWriter::Flush() { String body = boost::algorithm::join(m_DataBuffer, "\n"); m_DataBuffer.clear(); Stream::Ptr stream; try { stream = Connect(); } catch (const std::exception& ex) { Log(LogWarning, "InfluxDbWriter") << "Flush failed, cannot connect to InfluxDB."; return; } if (!stream) return; Url::Ptr url = new Url(); url->SetScheme(GetSslEnable() ? "https" : "http"); url->SetHost(GetHost()); url->SetPort(GetPort()); std::vector<String> path; path.emplace_back("write"); url->SetPath(path); url->AddQueryElement("db", GetDatabase()); url->AddQueryElement("precision", "s"); if (!GetUsername().IsEmpty()) url->AddQueryElement("u", GetUsername()); if (!GetPassword().IsEmpty()) url->AddQueryElement("p", GetPassword()); HttpRequest req(stream); req.RequestMethod = "POST"; req.RequestUrl = url; try { req.WriteBody(body.CStr(), body.GetLength()); req.Finish(); } catch (const std::exception& ex) { Log(LogWarning, "InfluxdbWriter") << "Cannot write to TCP socket on host '" << GetHost() << "' port '" << GetPort() << "'."; throw ex; } HttpResponse resp(stream, req); StreamReadContext context; try { while (resp.Parse(context, true) && !resp.Complete) ; /* Do nothing */ } catch (const std::exception& ex) { Log(LogWarning, "InfluxdbWriter") << "Failed to parse HTTP response from host '" << GetHost() << "' port '" << GetPort() << "': " << DiagnosticInformation(ex); throw ex; } if (!resp.Complete) { Log(LogWarning, "InfluxdbWriter") << "Failed to read a complete HTTP response from the InfluxDB server."; return; } if (resp.StatusCode != 204) { Log(LogWarning, "InfluxdbWriter") << "Unexpected response code: " << resp.StatusCode; String contentType = resp.Headers->Get("content-type"); if (contentType != "application/json") { Log(LogWarning, "InfluxdbWriter") << "Unexpected Content-Type: " << contentType; return; } size_t responseSize = resp.GetBodySize(); boost::scoped_array<char> buffer(new char[responseSize + 1]); resp.ReadBody(buffer.get(), responseSize); buffer.get()[responseSize] = '\0'; Dictionary::Ptr jsonResponse; try { jsonResponse = JsonDecode(buffer.get()); } catch (...) { Log(LogWarning, "InfluxdbWriter") << "Unable to parse JSON response:\n" << buffer.get(); return; } String error = jsonResponse->Get("error"); Log(LogCritical, "InfluxdbWriter") << "InfluxDB error message:\n" << error; return; } }
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; } } else { FilterUtility::CheckPermission(user, permission); objs.emplace_back(nullptr); } ArrayData results; Log(LogNotice, "ApiActionHandler") << "Running action " << actionName; bool verbose = false; 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; for (const Dictionary::Ptr& res : results) { if (res->Contains("code") && res->Get("code") == 200) { statusCode = 200; break; } } response.result(statusCode); Dictionary::Ptr result = new Dictionary({ { "results", new Array(std::move(results)) } }); HttpUtility::SendJsonBody(response, params, result); return true; }
void InfluxdbWriter::Flush(void) { Stream::Ptr stream = Connect(); // Unable to connect, play it safe and lose the data points // to avoid a memory leak if (!stream.get()) { m_DataBuffer->Clear(); return; } Url::Ptr url = new Url(); url->SetScheme(GetSslEnable() ? "https" : "http"); url->SetHost(GetHost()); url->SetPort(GetPort()); std::vector<String> path; path.push_back("write"); url->SetPath(path); url->AddQueryElement("db", GetDatabase()); url->AddQueryElement("precision", "s"); if (!GetUsername().IsEmpty()) url->AddQueryElement("u", GetUsername()); if (!GetPassword().IsEmpty()) url->AddQueryElement("p", GetPassword()); // Ensure you hold a lock against m_DataBuffer so that things // don't go missing after creating the body and clearing the buffer String body = Utility::Join(m_DataBuffer, '\n', false); m_DataBuffer->Clear(); HttpRequest req(stream); req.RequestMethod = "POST"; req.RequestUrl = url; try { req.WriteBody(body.CStr(), body.GetLength()); req.Finish(); } catch (const std::exception&) { Log(LogWarning, "InfluxdbWriter") << "Cannot write to TCP socket on host '" << GetHost() << "' port '" << GetPort() << "'."; return; } HttpResponse resp(stream, req); StreamReadContext context; try { resp.Parse(context, true); } catch (const std::exception&) { Log(LogWarning, "InfluxdbWriter") << "Cannot read from TCP socket from host '" << GetHost() << "' port '" << GetPort() << "'."; return; } if (resp.StatusCode != 204) { Log(LogWarning, "InfluxdbWriter") << "Unexpected response code " << resp.StatusCode; } }
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; } } }
int ConsoleCommand::RunScriptConsole(ScriptFrame& scriptFrame, const String& addr, const String& session, const String& commandOnce, const String& commandOnceFileName, bool syntaxOnly) { std::map<String, String> lines; int next_line = 1; #ifdef HAVE_EDITLINE char *homeEnv = getenv("HOME"); String historyPath; std::fstream historyfp; if (homeEnv) { historyPath = String(homeEnv) + "/.icinga2_history"; historyfp.open(historyPath.CStr(), std::fstream::in); String line; while (std::getline(historyfp, line.GetData())) add_history(line.CStr()); historyfp.close(); } #endif /* HAVE_EDITLINE */ l_ScriptFrame = &scriptFrame; l_Session = session; if (!addr.IsEmpty()) { Url::Ptr url; try { url = new Url(addr); } catch (const std::exception& ex) { Log(LogCritical, "ConsoleCommand", ex.what()); return EXIT_FAILURE; } const char *usernameEnv = getenv("ICINGA2_API_USERNAME"); const char *passwordEnv = getenv("ICINGA2_API_PASSWORD"); if (usernameEnv) url->SetUsername(usernameEnv); if (passwordEnv) url->SetPassword(passwordEnv); if (url->GetPort().IsEmpty()) url->SetPort("5665"); l_ApiClient = new ApiClient(url->GetHost(), url->GetPort(), url->GetUsername(), url->GetPassword()); } while (std::cin.good()) { String fileName; if (commandOnceFileName.IsEmpty()) fileName = "<" + Convert::ToString(next_line) + ">"; else fileName = commandOnceFileName; next_line++; bool continuation = false; std::string command; incomplete: std::string line; if (commandOnce.IsEmpty()) { #ifdef HAVE_EDITLINE std::ostringstream promptbuf; std::ostream& os = promptbuf; #else /* HAVE_EDITLINE */ std::ostream& os = std::cout; #endif /* HAVE_EDITLINE */ os << fileName; if (!continuation) os << " => "; else os << " .. "; #ifdef HAVE_EDITLINE String prompt = promptbuf.str(); char *cline; cline = readline(prompt.CStr()); if (!cline) break; if (commandOnce.IsEmpty() && cline[0] != '\0') { add_history(cline); if (!historyPath.IsEmpty()) { historyfp.open(historyPath.CStr(), std::fstream::out | std::fstream::app); historyfp << cline << "\n"; historyfp.close(); } } line = cline; free(cline); #else /* HAVE_EDITLINE */ std::getline(std::cin, line); #endif /* HAVE_EDITLINE */ } else line = commandOnce; if (!line.empty() && line[0] == '$') { if (line == "$continue" || line == "$quit" || line == "$exit") break; else if (line == "$help") std::cout << "Welcome to the Icinga 2 debug console.\n" "Usable commands:\n" " $continue Continue running Icinga 2 (script debugger).\n" " $quit, $exit Stop debugging and quit the console.\n" " $help Print this help.\n\n" "For more information on how to use this console, please consult the documentation at https://icinga.com/docs\n"; else std::cout << "Unknown debugger command: " << line << "\n"; continue; } if (!command.empty()) command += "\n"; command += line; std::unique_ptr<Expression> expr; try { lines[fileName] = command; Value result; if (!l_ApiClient) { expr = ConfigCompiler::CompileText(fileName, command); /* This relies on the fact that - for syntax errors - CompileText() * returns an AST where the top-level expression is a 'throw'. */ if (!syntaxOnly || dynamic_cast<ThrowExpression *>(expr.get())) { if (syntaxOnly) std::cerr << " => " << command << std::endl; result = Serialize(expr->Evaluate(scriptFrame), 0); } else result = true; } else { boost::mutex mutex; boost::condition_variable cv; bool ready = false; boost::exception_ptr eptr; l_ApiClient->ExecuteScript(l_Session, command, scriptFrame.Sandboxed, std::bind(&ConsoleCommand::ExecuteScriptCompletionHandler, std::ref(mutex), std::ref(cv), std::ref(ready), _1, _2, std::ref(result), std::ref(eptr))); { boost::mutex::scoped_lock lock(mutex); while (!ready) cv.wait(lock); } if (eptr) boost::rethrow_exception(eptr); } if (commandOnce.IsEmpty()) { std::cout << ConsoleColorTag(Console_ForegroundCyan); ConfigWriter::EmitValue(std::cout, 1, result); std::cout << ConsoleColorTag(Console_Normal) << "\n"; } else { std::cout << JsonEncode(result) << "\n"; break; } } catch (const ScriptError& ex) { if (ex.IsIncompleteExpression() && commandOnce.IsEmpty()) { continuation = true; goto incomplete; } DebugInfo di = ex.GetDebugInfo(); if (commandOnceFileName.IsEmpty() && lines.find(di.Path) != lines.end()) { String text = lines[di.Path]; std::vector<String> ulines = text.Split("\n"); for (int i = 1; i <= ulines.size(); i++) { int start, len; if (i == di.FirstLine) start = di.FirstColumn; else start = 0; if (i == di.LastLine) len = di.LastColumn - di.FirstColumn + 1; else len = ulines[i - 1].GetLength(); int offset; if (di.Path != fileName) { std::cout << di.Path << ": " << ulines[i - 1] << "\n"; offset = 2; } else offset = 4; if (i >= di.FirstLine && i <= di.LastLine) { std::cout << String(di.Path.GetLength() + offset, ' '); std::cout << String(start, ' ') << String(len, '^') << "\n"; } } } else { ShowCodeLocation(std::cout, di); } std::cout << ex.what() << "\n"; if (!commandOnce.IsEmpty()) return EXIT_FAILURE; } catch (const std::exception& ex) { std::cout << "Error: " << DiagnosticInformation(ex) << "\n"; if (!commandOnce.IsEmpty()) return EXIT_FAILURE; } } return EXIT_SUCCESS; }
/** * The entry point for the "console" CLI command. * * @returns An exit status. */ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::string>& ap) const { #ifdef HAVE_EDITLINE rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper; rl_completion_append_character = '\0'; #endif /* HAVE_EDITLINE */ String addr, session; ScriptFrame scriptFrame(true); session = Utility::NewUniqueID(); if (vm.count("sandbox")) scriptFrame.Sandboxed = true; scriptFrame.Self = scriptFrame.Locals; if (!vm.count("eval") && !vm.count("file")) std::cout << "Icinga 2 (version: " << Application::GetAppVersion() << ")\n" << "Type $help to view available commands.\n"; String addrEnv = Utility::GetFromEnvironment("ICINGA2_API_URL"); if (!addrEnv.IsEmpty()) addr = addrEnv; /* Initialize remote connect parameters. */ if (vm.count("connect")) { addr = vm["connect"].as<std::string>(); try { l_Url = new Url(addr); } catch (const std::exception& ex) { Log(LogCritical, "ConsoleCommand", ex.what()); return EXIT_FAILURE; } String usernameEnv = Utility::GetFromEnvironment("ICINGA2_API_USERNAME"); String passwordEnv = Utility::GetFromEnvironment("ICINGA2_API_PASSWORD"); if (!usernameEnv.IsEmpty()) l_Url->SetUsername(usernameEnv); if (!passwordEnv.IsEmpty()) l_Url->SetPassword(passwordEnv); if (l_Url->GetPort().IsEmpty()) l_Url->SetPort("5665"); /* User passed --connect and wants to run the expression via REST API. * Evaluate this now before any user input happens. */ try { l_TlsStream = ConsoleCommand::Connect(); } catch (const std::exception& ex) { return EXIT_FAILURE; } } String command; bool syntaxOnly = false; if (vm.count("syntax-only")) { if (vm.count("eval") || vm.count("file")) syntaxOnly = true; else { std::cerr << "The option --syntax-only can only be used in combination with --eval or --file." << std::endl; return EXIT_FAILURE; } } String commandFileName; if (vm.count("eval")) command = vm["eval"].as<std::string>(); else if (vm.count("file")) { commandFileName = vm["file"].as<std::string>(); try { std::ifstream fp(commandFileName.CStr()); fp.exceptions(std::ifstream::failbit | std::ifstream::badbit); command = String(std::istreambuf_iterator<char>(fp), std::istreambuf_iterator<char>()); } catch (const std::exception&) { std::cerr << "Could not read file '" << commandFileName << "'." << std::endl; return EXIT_FAILURE; } } return RunScriptConsole(scriptFrame, addr, session, command, commandFileName, syntaxOnly); }
/** * Sends the request via REST API and returns the parsed response. * * @param tlsStream Caller must prepare TLS stream/handshake. * @param url Fully prepared Url object. * @return A dictionary decoded from JSON. */ Dictionary::Ptr ConsoleCommand::SendRequest() { namespace beast = boost::beast; namespace http = beast::http; l_TlsStream = ConsoleCommand::Connect(); Defer s ([&]() { l_TlsStream->next_layer().shutdown(); }); http::request<http::string_body> request(http::verb::post, std::string(l_Url->Format(false)), 10); request.set(http::field::user_agent, "Icinga/DebugConsole/" + Application::GetAppVersion()); request.set(http::field::host, l_Url->GetHost() + ":" + l_Url->GetPort()); request.set(http::field::accept, "application/json"); request.set(http::field::authorization, "Basic " + Base64::Encode(l_Url->GetUsername() + ":" + l_Url->GetPassword())); try { http::write(*l_TlsStream, request); l_TlsStream->flush(); } catch (const std::exception &ex) { Log(LogWarning, "DebugConsole") << "Cannot write HTTP request to REST API at URL '" << l_Url->Format(true) << "': " << ex.what(); throw; } http::parser<false, http::string_body> parser; beast::flat_buffer buf; try { http::read(*l_TlsStream, buf, parser); } catch (const std::exception &ex) { Log(LogWarning, "DebugConsole") << "Failed to parse HTTP response from REST API at URL '" << l_Url->Format(true) << "': " << ex.what(); throw; } auto &response(parser.get()); /* Handle HTTP errors first. */ if (response.result() != http::status::ok) { String message = "HTTP request failed; Code: " + Convert::ToString(response.result()) + "; Body: " + response.body(); BOOST_THROW_EXCEPTION(ScriptError(message)); } Dictionary::Ptr jsonResponse; auto &body(response.body()); //Log(LogWarning, "Console") // << "Got response: " << response.body(); try { jsonResponse = JsonDecode(body); } catch (...) { String message = "Cannot parse JSON response body: " + response.body(); BOOST_THROW_EXCEPTION(ScriptError(message)); } return jsonResponse; }