From: Gunnar Beutner Date: Fri, 28 Aug 2015 23:16:16 +0000 (+0200) Subject: Implement the Icinga Studio application X-Git-Tag: v2.4.0~346 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=c37a23ccba8b620e694b23893e987a381b899377;p=icinga2 Implement the Icinga Studio application fixes #10042 --- diff --git a/.travis.yml b/.travis.yml index 03ff00591..d054f696a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ addons: - libmysqlclient-dev - libedit-dev - libyajl-dev + - libwxbase3.0-dev + - libwxgtk3.0-dev before_script: - mkdir build diff --git a/CMakeLists.txt b/CMakeLists.txt index ca7c2719f..2274a92db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ option(ICINGA2_WITH_HELLO "Build the hello module" OFF) option(ICINGA2_WITH_LIVESTATUS "Build the Livestatus module" ON) option(ICINGA2_WITH_NOTIFICATION "Build the notification module" ON) option(ICINGA2_WITH_PERFDATA "Build the perfdata module" ON) +option(ICINGA2_WITH_STUDIO "Build the Icinga Studio application" OFF) file(STRINGS icinga2.spec VERSION_LINE REGEX "^Version: ") string(REPLACE "Version: " "" ICINGA2_VERSION ${VERSION_LINE}) @@ -244,6 +245,10 @@ add_subdirectory(test) add_subdirectory(agent) add_subdirectory(plugins) +if(ICINGA2_WITH_STUDIO) + add_subdirectory(icinga-studio) +endif() + set(CPACK_PACKAGE_NAME "Icinga2") set(CPACK_PACKAGE_VENDOR "Icinga Development Team") set(CPACK_PACKAGE_VERSION ${ICINGA2_VERSION}) diff --git a/icinga-studio/CMakeLists.txt b/icinga-studio/CMakeLists.txt new file mode 100644 index 000000000..40394a24c --- /dev/null +++ b/icinga-studio/CMakeLists.txt @@ -0,0 +1,65 @@ +# Icinga 2 +# Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +set(wxWidgets_CONFIGURATION mswu) +find_package(wxWidgets COMPONENTS core base propgrid REQUIRED) +include(${wxWidgets_USE_FILE}) + +if(MSVC) + set(WindowsSources icinga.rc) +else() + set(WindowsSources "") +endif() + +add_executable(icinga-studio MACOSX_BUNDLE WIN32 icinga-studio.cpp + forms.cpp aboutform.cpp connectform.cpp mainform.cpp + icinga.icns api.cpp ${WindowsSources}) + +include_directories(${Boost_INCLUDE_DIRS}) +target_link_libraries(icinga-studio ${Boost_LIBRARIES} ${wxWidgets_LIBRARIES} base remote) + +if(APPLE) + set_source_files_properties(icinga.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) +endif() + +set_target_properties ( + icinga-studio PROPERTIES + INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2 + FOLDER Bin + OUTPUT_NAME icinga-studio + MACOSX_BUNDLE_INFO_STRING "Icinga Studio" + MACOSX_BUNDLE_BUNDLE_NAME "Icinga Studio" + MACOSX_BUNDLE_GUI_IDENTIFIER "Icinga Studio" + MACOSX_BUNDLE_ICON_FILE icinga.icns + MACOSX_BUNDLE_SHORT_VERSION_STRING "${GIT_VERSION}" + MACOSX_BUNDLE_LONG_VERSION_STRING "${GIT_VERSION}" + MACOSX_BUNDLE_COPYRIGHT "(c) Icinga Development Team" + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/MacOSXBundleInfo.plist.in" +) + +if(WIN32) + set(InstallPath "${CMAKE_INSTALL_SBINDIR}") +else() + set(InstallPath "${CMAKE_INSTALL_BINDIR}") +endif() + +install( + TARGETS icinga-studio + RUNTIME DESTINATION ${InstallPath} + BUNDLE DESTINATION ${InstallPath} +) + diff --git a/icinga-studio/IcingaStudio.fbp b/icinga-studio/IcingaStudio.fbp new file mode 100644 index 000000000..33038d07a --- /dev/null +++ b/icinga-studio/IcingaStudio.fbp @@ -0,0 +1,2128 @@ + + + + + + C++ + 1 + source_name + 0 + 0 + res + UTF-8 + connect + forms + 1000 + none + 0 + IcingaStudio + + . + + 1 + 1 + 1 + 1 + UI + 1 + 0 + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 1 + impl_virtual + + + + 0 + wxID_ANY + + 800,569 + MainFormBase + + 800,569 + wxDEFAULT_FRAME_STYLE + + Icinga Studio + + + + wxTAB_TRAVERSAL + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + + + 0 + wxID_ANY + MyMenuBar + + + m_MenuBar + protected + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &File + m_FileMenu + none + + + 0 + 1 + + wxID_EXIT + wxITEM_NORMAL + &Quit + m_QuitMenuItem + none + + + OnQuitClicked + + + + + &Help + m_HelpMenu + none + + + 0 + 1 + + wxID_ABOUT + wxITEM_NORMAL + &About Icinga Studio... + m_AboutMenuItem + none + + + OnAboutClicked + + + + + + + m_DialogSizer + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + + m_ConnectionDetailsSizer + wxHORIZONTAL + none + + 2 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_TypesTree + 1 + + + protected + 1 + + Resizable + 1 + 315,-1 + wxTR_DEFAULT_STYLE|wxTR_HIDE_ROOT + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OnTypeSelected + + + + + + + + 5 + wxEXPAND + 1 + + + m_ObjectDetailsSizer + wxVERTICAL + none + + 2 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_ObjectsList + 1 + + + protected + 1 + + Resizable + 1 + + wxLC_REPORT + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OnObjectSelected + + + + + + + + + + + + + + + + + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + + 1 + + 0 + 0 + wxID_ANY + 1 + + 0 + + + 0 + + 1 + m_PropertyGrid + 1 + + + protected + 1 + + Resizable + 1 + + wxPG_DEFAULT_STYLE + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + + 1 + + 0 + wxID_ANY + + + m_StatusBar + protected + + + wxST_SIZEGRIP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + ConnectFormBase + + -1,-1 + wxDEFAULT_DIALOG_STYLE + + Icinga Studio - Connect + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + m_DialogSizer + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_ConnectionDetailsPanel + 1 + + + none + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + + + + + + + + + + + + + + + + + + + + + + + wxID_ANY + Connection Details + + m_DetailsSizer + wxVERTICAL + none + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Host: + + 0 + + + 0 + + 1 + m_HostLabel + 1 + + + none + 1 + + Resizable + 1 + + + + 0 + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_OK + + 0 + + + + 0 + + 1 + m_HostText + 1 + + + protected + 1 + + Resizable + 1 + + + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Port: + + 0 + + + 0 + + 1 + m_PortLabel + 1 + + + none + 1 + + Resizable + 1 + + + + 0 + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_PortText + 1 + + + protected + 1 + + Resizable + 1 + + + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + API User: + + 0 + + + 0 + + 1 + m_UserLabel + 1 + + + none + 1 + + Resizable + 1 + + + + 0 + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_UserText + 1 + + + protected + 1 + + Resizable + 1 + + + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + API Password: + + 0 + + + 0 + + 1 + m_PasswordLabel + 1 + + + none + 1 + + Resizable + 1 + + + + 0 + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_PasswordText + 1 + + + protected + 1 + + Resizable + 1 + + wxTE_PASSWORD + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + You can find the username and password for the default user in /etc/icinga2/conf.d/api-users.conf. + + 0 + + + 0 + + 1 + m_InfoLabel + 1 + + + none + 1 + + Resizable + 1 + + + + 0 + + + + + 270 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_ButtonsPanel + 1 + + + none + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + + + + + + + + + + + + + + + + + + + + + + + + m_ButtonsSizer + wxHORIZONTAL + none + + 5 + wxEXPAND + 1 + + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + + m_Buttons + none + + + + + + + + + + + + + + + + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + AboutFormBase + + -1,-1 + wxDEFAULT_DIALOG_STYLE + + About Icinga Studio + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + m_DialogSizer + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + + m_InfoSizer + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + Load From Embedded File; icinga.xpm + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_ProductIcon + 1 + + + none + 1 + + Resizable + 1 + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxEXPAND + 1 + + + m_AboutInfoSizer + wxVERTICAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Icinga Studio + + 0 + + + 0 + + 1 + m_ProductNameLabel + 1 + + + none + 1 + + Resizable + 1 + + + + 0 + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Version + + 0 + + + 0 + + 1 + m_VersionLabel + 1 + + + protected + 1 + + Resizable + 1 + + + + 0 + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Copyright (c) 2015 Icinga Development Team + + 0 + + + 0 + + 1 + m_CopyrightLabel + 1 + + + none + 1 + + Resizable + 1 + + + + 0 + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_ButtonsPanel + 1 + + + none + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + + + + + + + + + + + + + + + + + + + + + + + + m_ButtonsSizer + wxVERTICAL + none + + 5 + wxEXPAND + 0 + + 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + + m_Buttons + none + + + + + + + + + + + + + + + + + diff --git a/icinga-studio/MacOSXBundleInfo.plist.in b/icinga-studio/MacOSXBundleInfo.plist.in new file mode 100644 index 000000000..681035201 --- /dev/null +++ b/icinga-studio/MacOSXBundleInfo.plist.in @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + NSHighResolutionCapable + + + diff --git a/icinga-studio/aboutform.cpp b/icinga-studio/aboutform.cpp new file mode 100644 index 000000000..d47b22c1b --- /dev/null +++ b/icinga-studio/aboutform.cpp @@ -0,0 +1,30 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#include "base/application.hpp" +#include "icinga-studio/aboutform.hpp" + +using namespace icinga; + +AboutForm::AboutForm(wxWindow *parent) + : AboutFormBase(parent) +{ + std::string version = "Version " + Application::GetVersion(); + m_VersionLabel->SetLabelText(version); +} diff --git a/icinga-studio/aboutform.hpp b/icinga-studio/aboutform.hpp new file mode 100644 index 000000000..58c6da516 --- /dev/null +++ b/icinga-studio/aboutform.hpp @@ -0,0 +1,36 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#ifndef ABOUTFORM_H +#define ABOUTFORM_H + +#include "icinga-studio/forms.h" + +namespace icinga +{ + +class AboutForm : public AboutFormBase +{ +public: + AboutForm(wxWindow *parent); +}; + +} + +#endif /* ABOUTFORM_H */ \ No newline at end of file diff --git a/icinga-studio/api.cpp b/icinga-studio/api.cpp new file mode 100644 index 000000000..ac9073dd1 --- /dev/null +++ b/icinga-studio/api.cpp @@ -0,0 +1,165 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#include "icinga-studio/api.hpp" +#include "remote/base64.hpp" +#include "base/json.hpp" +#include "base/logger.hpp" +#include "base/exception.hpp" +#include + +using namespace icinga; + +ApiClient::ApiClient(const String& host, const String& port, + const String& user, const String& password) + : m_Connection(new HttpClientConnection(host, port, true)), m_User(user), m_Password(password) +{ + m_Connection->Start(); +} + +void ApiClient::GetTypes(const TypesCompletionCallback& callback) const +{ + boost::shared_ptr req = m_Connection->NewRequest(); + req->RequestMethod = "GET"; + req->RequestUrl = new Url("https://" + m_Connection->GetHost() + ":" + m_Connection->GetPort() + "/v1/types"); + req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password)); + m_Connection->SubmitRequest(req, boost::bind(TypesHttpCompletionCallback, _1, _2, callback)); +} + +void ApiClient::TypesHttpCompletionCallback(HttpRequest& request, HttpResponse& response, + const TypesCompletionCallback& callback) +{ + Dictionary::Ptr result; + + String body; + char buffer[1024]; + size_t count; + + while ((count = response.ReadBody(buffer, sizeof(buffer))) > 0) + body += String(buffer, buffer + count); + + std::vector types; + + try { + result = JsonDecode(body); + + Array::Ptr results = result->Get("results"); + + ObjectLock olock(results); + BOOST_FOREACH(const Dictionary::Ptr typeInfo, results) + { + ApiType::Ptr type = new ApiType();; + type->Abstract = typeInfo->Get("abstract"); + type->BaseName = typeInfo->Get("base"); + type->Name = typeInfo->Get("name"); + type->PluralName = typeInfo->Get("plural_name"); + // TODO: attributes + types.push_back(type); + } + } catch (const std::exception& ex) { + Log(LogCritical, "ApiClient") + << "Error while decoding response: " << DiagnosticInformation(ex); + } + + callback(types); +} + +void ApiClient::GetObjects(const String& pluralType, const ObjectsCompletionCallback& callback, + const std::vector& names, const std::vector& attrs) const +{ + String url = "https://" + m_Connection->GetHost() + ":" + m_Connection->GetPort() + "/v1/" + pluralType; + String qp; + + BOOST_FOREACH(const String& name, names) { + if (!qp.IsEmpty()) + qp += "&"; + + qp += pluralType.ToLower() + "=" + name; + } + + BOOST_FOREACH(const String& attr, attrs) { + if (!qp.IsEmpty()) + qp += "&"; + + qp += "attrs[]=" + attr; + } + + boost::shared_ptr req = m_Connection->NewRequest(); + req->RequestMethod = "GET"; + req->RequestUrl = new Url(url + "?" + qp); + req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password)); + m_Connection->SubmitRequest(req, boost::bind(ObjectsHttpCompletionCallback, _1, _2, callback)); +} + +void ApiClient::ObjectsHttpCompletionCallback(HttpRequest& request, + HttpResponse& response, const ObjectsCompletionCallback& callback) +{ + Dictionary::Ptr result; + + String body; + char buffer[1024]; + size_t count; + + while ((count = response.ReadBody(buffer, sizeof(buffer))) > 0) + body += String(buffer, buffer + count); + + std::vector objects; + + try { + result = JsonDecode(body); + + Array::Ptr results = result->Get("results"); + + ObjectLock olock(results); + BOOST_FOREACH(const Dictionary::Ptr objectInfo, results) + { + ApiObject::Ptr object = new ApiObject(); + + Dictionary::Ptr attrs = objectInfo->Get("attrs"); + + { + ObjectLock olock(attrs); + BOOST_FOREACH(const Dictionary::Pair& kv, attrs) + { + object->Attrs[kv.first] = kv.second; + } + } + + Array::Ptr used_by = objectInfo->Get("used_by"); + + { + ObjectLock olock(used_by); + BOOST_FOREACH(const Dictionary::Ptr& refInfo, used_by) + { + ApiObjectReference ref; + ref.Name = refInfo->Get("name"); + ref.Type = refInfo->Get("type"); + object->UsedBy.push_back(ref); + } + } + + objects.push_back(object); + } + } catch (const std::exception& ex) { + Log(LogCritical, "ApiClient") + << "Error while decoding response: " << DiagnosticInformation(ex); + } + + callback(objects); +} \ No newline at end of file diff --git a/icinga-studio/api.hpp b/icinga-studio/api.hpp new file mode 100644 index 000000000..c3bb0351a --- /dev/null +++ b/icinga-studio/api.hpp @@ -0,0 +1,111 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#ifndef API_H +#define API_H + +#include "remote/httpclientconnection.hpp" +#include "base/value.hpp" +#include + +namespace icinga +{ + +struct ApiFieldAttributes +{ +public: + bool Config; + bool Internal; + bool Required; + bool State; +}; + +class ApiType; + +struct ApiField +{ +public: + String Name; + int ID; + int ArrayRank; + ApiFieldAttributes FieldAttributes; + String TypeName; + intrusive_ptr Type; +}; + +class ApiType : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(ApiType); + + String Name; + String PluralName; + String BaseName; + ApiType::Ptr Base; + bool Abstract; + std::map Fields; + std::vector PrototypeKeys; +}; + +struct ApiObjectReference +{ +public: + String Name; + String Type; +}; + +struct ApiObject : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(ApiObject); + + std::map Attrs; + std::vector UsedBy; +}; + +class ApiClient : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(ApiClient); + + ApiClient(const String& host, const String& port, + const String& user, const String& password); + + typedef boost::function&)> TypesCompletionCallback; + void GetTypes(const TypesCompletionCallback& callback) const; + + typedef boost::function&)> ObjectsCompletionCallback; + void GetObjects(const String& pluralType, const ObjectsCompletionCallback& callback, + const std::vector& names = std::vector(), + const std::vector& attrs = std::vector()) const; + +private: + HttpClientConnection::Ptr m_Connection; + String m_User; + String m_Password; + + static void TypesHttpCompletionCallback(HttpRequest& request, + HttpResponse& response, const TypesCompletionCallback& callback); + static void ObjectsHttpCompletionCallback(HttpRequest& request, + HttpResponse& response, const ObjectsCompletionCallback& callback); +}; + +} + +#endif /* API_H */ \ No newline at end of file diff --git a/icinga-studio/connectform.cpp b/icinga-studio/connectform.cpp new file mode 100644 index 000000000..1a38ee1b3 --- /dev/null +++ b/icinga-studio/connectform.cpp @@ -0,0 +1,63 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#include "icinga-studio/connectform.hpp" +#include +#include + +using namespace icinga; + +ConnectForm::ConnectForm(wxWindow *parent, const Url::Ptr& url) + : ConnectFormBase(parent) +{ +#ifdef _WIN32 + SetIcon(wxICON(icinga)); +#endif /* _WIN32 */ + + std::string authority = url->GetAuthority(); + + std::vector tokens; + boost::algorithm::split(tokens, authority, boost::is_any_of("@")); + + if (tokens.size() > 1) { + std::vector userinfo; + boost::algorithm::split(userinfo, tokens[0], boost::is_any_of(":")); + + m_UserText->SetValue(userinfo[0]); + m_PasswordText->SetValue(userinfo[1]); + } + + std::vector 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"); +} + +Url::Ptr ConnectForm::GetUrl(void) const +{ + wxString url = "https://" + m_UserText->GetValue() + ":" + m_PasswordText->GetValue() + + "@" + m_HostText->GetValue() + ":" + m_PortText->GetValue() + "/"; + + return new Url(url.ToStdString()); +} diff --git a/icinga-studio/connectform.hpp b/icinga-studio/connectform.hpp new file mode 100644 index 000000000..8c0a9a9a5 --- /dev/null +++ b/icinga-studio/connectform.hpp @@ -0,0 +1,39 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#ifndef CONNECTFORM_H +#define CONNECTFORM_H + +#include "remote/url.hpp" +#include "icinga-studio/forms.h" + +namespace icinga +{ + +class ConnectForm : public ConnectFormBase +{ +public: + ConnectForm(wxWindow *parent, const Url::Ptr& url); + + Url::Ptr GetUrl(void) const; +}; + +} + +#endif /* CONNECTFORM_H */ \ No newline at end of file diff --git a/icinga-studio/forms.cpp b/icinga-studio/forms.cpp new file mode 100644 index 000000000..ec3b25ff3 --- /dev/null +++ b/icinga-studio/forms.cpp @@ -0,0 +1,242 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version Jun 17 2015) +// http://www.wxformbuilder.org/ +// +// PLEASE DO "NOT" EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#include "forms.h" + +#include "icinga.xpm" + +/////////////////////////////////////////////////////////////////////////// + +MainFormBase::MainFormBase( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxFrame( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( 800,569 ), wxDefaultSize ); + + m_MenuBar = new wxMenuBar( 0 ); + wxMenu* m_FileMenu; + m_FileMenu = new wxMenu(); + wxMenuItem* m_QuitMenuItem; + m_QuitMenuItem = new wxMenuItem( m_FileMenu, wxID_EXIT, wxString( wxT("&Quit") ) , wxEmptyString, wxITEM_NORMAL ); + m_FileMenu->Append( m_QuitMenuItem ); + + m_MenuBar->Append( m_FileMenu, wxT("&File") ); + + wxMenu* m_HelpMenu; + m_HelpMenu = new wxMenu(); + wxMenuItem* m_AboutMenuItem; + m_AboutMenuItem = new wxMenuItem( m_HelpMenu, wxID_ABOUT, wxString( wxT("&About Icinga Studio...") ) , wxEmptyString, wxITEM_NORMAL ); + m_HelpMenu->Append( m_AboutMenuItem ); + + m_MenuBar->Append( m_HelpMenu, wxT("&Help") ); + + this->SetMenuBar( m_MenuBar ); + + wxBoxSizer* m_DialogSizer; + m_DialogSizer = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* m_ConnectionDetailsSizer; + m_ConnectionDetailsSizer = new wxBoxSizer( wxHORIZONTAL ); + + m_TypesTree = new wxTreeCtrl( this, wxID_ANY, wxDefaultPosition, wxSize( 315,-1 ), wxTR_DEFAULT_STYLE|wxTR_HIDE_ROOT ); + m_ConnectionDetailsSizer->Add( m_TypesTree, 0, wxALL|wxEXPAND, 2 ); + + wxBoxSizer* m_ObjectDetailsSizer; + m_ObjectDetailsSizer = new wxBoxSizer( wxVERTICAL ); + + m_ObjectsList = new wxListCtrl( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT ); + m_ObjectDetailsSizer->Add( m_ObjectsList, 1, wxALL|wxEXPAND, 2 ); + + m_PropertyGrid = new wxPropertyGrid(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxPG_DEFAULT_STYLE); + m_ObjectDetailsSizer->Add( m_PropertyGrid, 1, wxALL|wxEXPAND, 5 ); + + + m_ConnectionDetailsSizer->Add( m_ObjectDetailsSizer, 1, wxEXPAND, 5 ); + + + m_DialogSizer->Add( m_ConnectionDetailsSizer, 1, wxEXPAND, 5 ); + + + this->SetSizer( m_DialogSizer ); + this->Layout(); + m_StatusBar = this->CreateStatusBar( 1, wxST_SIZEGRIP, wxID_ANY ); + + this->Centre( wxBOTH ); + + // Connect Events + this->Connect( m_QuitMenuItem->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainFormBase::OnQuitClicked ) ); + this->Connect( m_AboutMenuItem->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainFormBase::OnAboutClicked ) ); + m_TypesTree->Connect( wxEVT_COMMAND_TREE_SEL_CHANGED, wxTreeEventHandler( MainFormBase::OnTypeSelected ), NULL, this ); + m_ObjectsList->Connect( wxEVT_COMMAND_LIST_ITEM_SELECTED, wxListEventHandler( MainFormBase::OnObjectSelected ), NULL, this ); +} + +MainFormBase::~MainFormBase() +{ + // Disconnect Events + this->Disconnect( wxID_EXIT, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainFormBase::OnQuitClicked ) ); + this->Disconnect( wxID_ABOUT, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainFormBase::OnAboutClicked ) ); + m_TypesTree->Disconnect( wxEVT_COMMAND_TREE_SEL_CHANGED, wxTreeEventHandler( MainFormBase::OnTypeSelected ), NULL, this ); + m_ObjectsList->Disconnect( wxEVT_COMMAND_LIST_ITEM_SELECTED, wxListEventHandler( MainFormBase::OnObjectSelected ), NULL, this ); + +} + +ConnectFormBase::ConnectFormBase( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + + wxBoxSizer* m_DialogSizer; + m_DialogSizer = new wxBoxSizer( wxVERTICAL ); + + wxPanel* m_ConnectionDetailsPanel; + m_ConnectionDetailsPanel = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxStaticBoxSizer* m_DetailsSizer; + m_DetailsSizer = new wxStaticBoxSizer( new wxStaticBox( m_ConnectionDetailsPanel, wxID_ANY, wxT("Connection Details") ), wxVERTICAL ); + + wxStaticText* m_HostLabel; + m_HostLabel = new wxStaticText( m_DetailsSizer->GetStaticBox(), wxID_ANY, wxT("Host:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_HostLabel->Wrap( -1 ); + m_DetailsSizer->Add( m_HostLabel, 0, wxALL, 5 ); + + m_HostText = new wxTextCtrl( m_DetailsSizer->GetStaticBox(), wxID_OK, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + m_DetailsSizer->Add( m_HostText, 0, wxALL|wxEXPAND, 5 ); + + wxStaticText* m_PortLabel; + m_PortLabel = new wxStaticText( m_DetailsSizer->GetStaticBox(), wxID_ANY, wxT("Port:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_PortLabel->Wrap( -1 ); + m_DetailsSizer->Add( m_PortLabel, 0, wxALL, 5 ); + + m_PortText = new wxTextCtrl( m_DetailsSizer->GetStaticBox(), wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + m_DetailsSizer->Add( m_PortText, 0, wxALL, 5 ); + + wxStaticText* m_UserLabel; + m_UserLabel = new wxStaticText( m_DetailsSizer->GetStaticBox(), wxID_ANY, wxT("API User:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_UserLabel->Wrap( -1 ); + m_DetailsSizer->Add( m_UserLabel, 0, wxALL, 5 ); + + m_UserText = new wxTextCtrl( m_DetailsSizer->GetStaticBox(), wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + m_DetailsSizer->Add( m_UserText, 0, wxALL|wxEXPAND, 5 ); + + wxStaticText* m_PasswordLabel; + m_PasswordLabel = new wxStaticText( m_DetailsSizer->GetStaticBox(), wxID_ANY, wxT("API Password:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_PasswordLabel->Wrap( -1 ); + m_DetailsSizer->Add( m_PasswordLabel, 0, wxALL, 5 ); + + m_PasswordText = new wxTextCtrl( m_DetailsSizer->GetStaticBox(), wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD ); + m_DetailsSizer->Add( m_PasswordText, 0, wxALL|wxEXPAND, 5 ); + + wxStaticText* m_InfoLabel; + m_InfoLabel = new wxStaticText( m_DetailsSizer->GetStaticBox(), wxID_ANY, wxT("You can find the username and password for the default user in /etc/icinga2/conf.d/api-users.conf."), wxDefaultPosition, wxDefaultSize, 0 ); + m_InfoLabel->Wrap( 270 ); + m_DetailsSizer->Add( m_InfoLabel, 0, wxALL, 5 ); + + + m_ConnectionDetailsPanel->SetSizer( m_DetailsSizer ); + m_ConnectionDetailsPanel->Layout(); + m_DetailsSizer->Fit( m_ConnectionDetailsPanel ); + m_DialogSizer->Add( m_ConnectionDetailsPanel, 1, wxEXPAND | wxALL, 5 ); + + wxPanel* m_ButtonsPanel; + m_ButtonsPanel = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* m_ButtonsSizer; + m_ButtonsSizer = new wxBoxSizer( wxHORIZONTAL ); + + wxStdDialogButtonSizer* m_Buttons; + wxButton* m_ButtonsOK; + wxButton* m_ButtonsCancel; + m_Buttons = new wxStdDialogButtonSizer(); + m_ButtonsOK = new wxButton( m_ButtonsPanel, wxID_OK ); + m_Buttons->AddButton( m_ButtonsOK ); + m_ButtonsCancel = new wxButton( m_ButtonsPanel, wxID_CANCEL ); + m_Buttons->AddButton( m_ButtonsCancel ); + m_Buttons->Realize(); + + m_ButtonsSizer->Add( m_Buttons, 1, wxEXPAND, 5 ); + + + m_ButtonsPanel->SetSizer( m_ButtonsSizer ); + m_ButtonsPanel->Layout(); + m_ButtonsSizer->Fit( m_ButtonsPanel ); + m_DialogSizer->Add( m_ButtonsPanel, 0, wxEXPAND | wxALL, 5 ); + + + this->SetSizer( m_DialogSizer ); + this->Layout(); + m_DialogSizer->Fit( this ); + + this->Centre( wxBOTH ); +} + +ConnectFormBase::~ConnectFormBase() +{ +} + +AboutFormBase::AboutFormBase( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + + wxBoxSizer* m_DialogSizer; + m_DialogSizer = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* m_InfoSizer; + m_InfoSizer = new wxBoxSizer( wxHORIZONTAL ); + + wxStaticBitmap* m_ProductIcon; + m_ProductIcon = new wxStaticBitmap( this, wxID_ANY, wxBitmap( icinga_xpm ), wxDefaultPosition, wxDefaultSize, 0 ); + m_InfoSizer->Add( m_ProductIcon, 0, wxALL, 5 ); + + wxBoxSizer* m_AboutInfoSizer; + m_AboutInfoSizer = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* m_ProductNameLabel; + m_ProductNameLabel = new wxStaticText( this, wxID_ANY, wxT("Icinga Studio"), wxDefaultPosition, wxDefaultSize, 0 ); + m_ProductNameLabel->Wrap( -1 ); + m_AboutInfoSizer->Add( m_ProductNameLabel, 0, wxALL, 5 ); + + m_VersionLabel = new wxStaticText( this, wxID_ANY, wxT("Version"), wxDefaultPosition, wxDefaultSize, 0 ); + m_VersionLabel->Wrap( -1 ); + m_AboutInfoSizer->Add( m_VersionLabel, 0, wxALL, 5 ); + + wxStaticText* m_CopyrightLabel; + m_CopyrightLabel = new wxStaticText( this, wxID_ANY, wxT("Copyright (c) 2015 Icinga Development Team"), wxDefaultPosition, wxDefaultSize, 0 ); + m_CopyrightLabel->Wrap( -1 ); + m_AboutInfoSizer->Add( m_CopyrightLabel, 0, wxALL, 5 ); + + + m_InfoSizer->Add( m_AboutInfoSizer, 1, wxEXPAND, 5 ); + + + m_DialogSizer->Add( m_InfoSizer, 1, wxEXPAND, 5 ); + + wxPanel* m_ButtonsPanel; + m_ButtonsPanel = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* m_ButtonsSizer; + m_ButtonsSizer = new wxBoxSizer( wxVERTICAL ); + + wxStdDialogButtonSizer* m_Buttons; + wxButton* m_ButtonsOK; + m_Buttons = new wxStdDialogButtonSizer(); + m_ButtonsOK = new wxButton( m_ButtonsPanel, wxID_OK ); + m_Buttons->AddButton( m_ButtonsOK ); + m_Buttons->Realize(); + + m_ButtonsSizer->Add( m_Buttons, 0, wxEXPAND, 5 ); + + + m_ButtonsPanel->SetSizer( m_ButtonsSizer ); + m_ButtonsPanel->Layout(); + m_ButtonsSizer->Fit( m_ButtonsPanel ); + m_DialogSizer->Add( m_ButtonsPanel, 0, wxEXPAND | wxALL, 5 ); + + + this->SetSizer( m_DialogSizer ); + this->Layout(); + m_DialogSizer->Fit( this ); + + this->Centre( wxBOTH ); +} + +AboutFormBase::~AboutFormBase() +{ +} diff --git a/icinga-studio/forms.h b/icinga-studio/forms.h new file mode 100644 index 000000000..613b7d469 --- /dev/null +++ b/icinga-studio/forms.h @@ -0,0 +1,105 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version Jun 17 2015) +// http://www.wxformbuilder.org/ +// +// PLEASE DO "NOT" EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#ifndef __FORMS_H__ +#define __FORMS_H__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +/// Class MainFormBase +/////////////////////////////////////////////////////////////////////////////// +class MainFormBase : public wxFrame +{ + private: + + protected: + wxMenuBar* m_MenuBar; + wxTreeCtrl* m_TypesTree; + wxListCtrl* m_ObjectsList; + wxPropertyGrid* m_PropertyGrid; + wxStatusBar* m_StatusBar; + + // Virtual event handlers, overide them in your derived class + virtual void OnQuitClicked( wxCommandEvent& event ) { event.Skip(); } + virtual void OnAboutClicked( wxCommandEvent& event ) { event.Skip(); } + virtual void OnTypeSelected( wxTreeEvent& event ) { event.Skip(); } + virtual void OnObjectSelected( wxListEvent& event ) { event.Skip(); } + + + public: + + MainFormBase( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Icinga Studio"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 800,569 ), long style = wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL ); + + ~MainFormBase(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class ConnectFormBase +/////////////////////////////////////////////////////////////////////////////// +class ConnectFormBase : public wxDialog +{ + private: + + protected: + wxTextCtrl* m_HostText; + wxTextCtrl* m_PortText; + wxTextCtrl* m_UserText; + wxTextCtrl* m_PasswordText; + + public: + + ConnectFormBase( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Icinga Studio - Connect"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1,-1 ), long style = wxDEFAULT_DIALOG_STYLE ); + ~ConnectFormBase(); + +}; + +/////////////////////////////////////////////////////////////////////////////// +/// Class AboutFormBase +/////////////////////////////////////////////////////////////////////////////// +class AboutFormBase : public wxDialog +{ + private: + + protected: + wxStaticText* m_VersionLabel; + + public: + + AboutFormBase( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("About Icinga Studio"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1,-1 ), long style = wxDEFAULT_DIALOG_STYLE ); + ~AboutFormBase(); + +}; + +#endif //__FORMS_H__ diff --git a/icinga-studio/icinga-studio.cpp b/icinga-studio/icinga-studio.cpp new file mode 100644 index 000000000..ff98cfccd --- /dev/null +++ b/icinga-studio/icinga-studio.cpp @@ -0,0 +1,66 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#include "icinga-studio/connectform.hpp" +#include "icinga-studio/mainform.hpp" +#include "base/application.hpp" +#include +#include +#include + +using namespace icinga; + +class IcingaStudio : public wxApp +{ +public: + 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(); + wUrl = url; + config.Write("url", wUrl); + } else { + pUrl = new Url(argv[1].ToStdString()); + } + + MainForm *m = new MainForm(NULL, pUrl); + m->Show(); + + return true; + } +}; + +wxIMPLEMENT_APP(IcingaStudio); diff --git a/icinga-studio/icinga.icns b/icinga-studio/icinga.icns new file mode 100644 index 000000000..67f009e32 Binary files /dev/null and b/icinga-studio/icinga.icns differ diff --git a/icinga-studio/icinga.ico b/icinga-studio/icinga.ico new file mode 100644 index 000000000..6ff7e9029 Binary files /dev/null and b/icinga-studio/icinga.ico differ diff --git a/icinga-studio/icinga.rc b/icinga-studio/icinga.rc new file mode 100644 index 000000000..abcda3f4e --- /dev/null +++ b/icinga-studio/icinga.rc @@ -0,0 +1,34 @@ +#include +#include "icinga-version.h" + +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +icinga ICON "icinga.ico" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1,0,0,0 +PRODUCTVERSION 1,0,0,0 +FILEOS VOS__WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", "Icinga Development Team" + VALUE "FileDescription", "Icinga Studio" + VALUE "FileVersion", VERSION + VALUE "InternalName", "icinga-studio.exe" + VALUE "LegalCopyright", "© Icinga Development Team" + VALUE "OriginalFilename", "icinga-studio.exe" + VALUE "ProductName", "Icinga 2" + VALUE "ProductVersion", VERSION + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 0x04E4 + END +END \ No newline at end of file diff --git a/icinga-studio/icinga.xpm b/icinga-studio/icinga.xpm new file mode 100644 index 000000000..271363db7 --- /dev/null +++ b/icinga-studio/icinga.xpm @@ -0,0 +1,40 @@ +/* XPM */ +static const char *icinga_xpm[] = { +"32 32 5 1", +" c None", +". c #808080", +"+ c #000000", +"@ c #C0C0C0", +"# c #FFFFFF", +" ", +" .++++++++++++++++++++++++. ", +" .+++++++++++++++..+++++++++. ", +" .+++++++++++++++@##@+++++++++. ", +" +++++++++++++++.####.+++++++++ ", +" +++++++++++++++.####.+++++++++ ", +" ++++++++++++++++####++++++++++ ", +" ++++++++++++++++@@..++++++++++ ", +" +++++..+++++++++#.++++++++++++ ", +" ++++.##@+++++++@#+++++++++++++ ", +" ++++.###+++++++#.+++++++++.+++ ", +" ++++.###@++.@@@#+++++++++@##.+ ", +" +++++++.@#######.+++++++.###@+ ", +" +++++++++########++++..@####.+ ", +" ++++++++.########@@###@...@.++ ", +" ++++++++.#########@..+++++++++ ", +" ++++++++.########@++++++++++++ ", +" ++++++++.########.++++++++++++ ", +" +++++++++########+++++++++++++ ", +" +++++++++.######.+++++++++++++ ", +" +++++++++.#....#.+++++++++++++ ", +" ++++++++.#.++++.#.++++++++++++ ", +" ++++++++@@++++++##.+++++++++++ ", +" ++++@##@#+++++++##@+++++++++++ ", +" +++@####@+++++++..++++++++++++ ", +" +++######.++++++++++++++++++++ ", +" +++######.++++++++++++++++++++ ", +" +++######+++++++++++++++++++++ ", +" .++.####.++++++++++++++++++++. ", +" .+++..+++++++++++++++++++++. ", +" .++++++++++++++++++++++++. ", +" "}; diff --git a/icinga-studio/mainform.cpp b/icinga-studio/mainform.cpp new file mode 100644 index 000000000..18aea2674 --- /dev/null +++ b/icinga-studio/mainform.cpp @@ -0,0 +1,258 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#include "icinga-studio/mainform.hpp" +#include "icinga-studio/aboutform.hpp" +#include +#include +#include + +using namespace icinga; + +MainForm::MainForm(wxWindow *parent, const Url::Ptr& url) + : MainFormBase(parent) +{ +#ifdef _WIN32 + SetIcon(wxICON(icinga)); + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); +#endif /* _WIN32 */ + + String host, port, user, pass; + + std::string authority = url->GetAuthority(); + + std::vector tokens; + boost::algorithm::split(tokens, authority, boost::is_any_of("@")); + + if (tokens.size() > 1) { + std::vector userinfo; + boost::algorithm::split(userinfo, tokens[0], boost::is_any_of(":")); + + user = userinfo[0]; + pass = userinfo[1]; + } + + std::vector hostport; + boost::algorithm::split(hostport, tokens.size() > 1 ? tokens[1] : tokens[0], boost::is_any_of(":")); + + host = hostport[0]; + + if (hostport.size() > 1) + port = hostport[1]; + else + port = "5665"; + + m_ApiClient = new ApiClient(host, port, user, pass); + m_ApiClient->GetTypes(boost::bind(&MainForm::TypesCompletionHandler, this, _1, true)); + + std::string title = host; + + if (port != "5665") + title += +":" + port; + + title += " - Icinga Studio"; + SetTitle(title); + + m_ObjectsList->InsertColumn(0, "Name", 0, 300); +} + +void MainForm::TypesCompletionHandler(const std::vector& types, bool forward) +{ + if (forward) { + CallAfter(boost::bind(&MainForm::TypesCompletionHandler, this, types, false)); + return; + } + + m_TypesTree->DeleteAllItems(); + wxTreeItemId rootNode = m_TypesTree->AddRoot("root"); + + bool all = false; + std::map items; + + m_Types.clear(); + + while (!all) { + all = true; + + BOOST_FOREACH(const ApiType::Ptr& type, types) { + std::string name = type->Name; + + if (items.find(name) != items.end()) + continue; + + all = false; + + wxTreeItemId parent; + + if (type->BaseName.IsEmpty()) + parent = rootNode; + else { + std::map::const_iterator it = items.find(type->BaseName); + + if (it == items.end()) + continue; + + parent = it->second; + } + + m_Types[name] = type; + items[name] = m_TypesTree->AppendItem(parent, name, 0); + } + } +} + +void MainForm::OnTypeSelected(wxTreeEvent& event) +{ + wxTreeItemId selectedId = m_TypesTree->GetSelection(); + wxString typeName = m_TypesTree->GetItemText(selectedId); + ApiType::Ptr type = m_Types[typeName.ToStdString()]; + + std::vector attrs; + attrs.push_back(type->Name.ToLower() + ".__name"); + + m_ApiClient->GetObjects(type->PluralName, boost::bind(&MainForm::ObjectsCompletionHandler, this, _1, true), + std::vector(), attrs); +} + +void MainForm::ObjectsCompletionHandler(const std::vector& objects, bool forward) +{ + if (forward) { + CallAfter(boost::bind(&MainForm::ObjectsCompletionHandler, this, objects, false)); + return; + } + + wxTreeItemId selectedId = m_TypesTree->GetSelection(); + wxString typeName = m_TypesTree->GetItemText(selectedId); + ApiType::Ptr type = m_Types[typeName.ToStdString()]; + + String nameAttr = type->Name.ToLower() + ".__name"; + + m_ObjectsList->DeleteAllItems(); + + BOOST_FOREACH(const ApiObject::Ptr& object, objects) { + std::map::const_iterator it = object->Attrs.find(nameAttr); + if (it == object->Attrs.end()) + continue; + String name = it->second; + m_ObjectsList->InsertItem(0, name.GetData()); + } +} + +void MainForm::OnObjectSelected(wxListEvent& event) +{ + wxTreeItemId selectedId = m_TypesTree->GetSelection(); + wxString typeName = m_TypesTree->GetItemText(selectedId); + ApiType::Ptr type = m_Types[typeName.ToStdString()]; + + long itemIndex = -1; + std::string objectName; + + while ((itemIndex = m_ObjectsList->GetNextItem(itemIndex, + wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != wxNOT_FOUND) { + objectName = m_ObjectsList->GetItemText(itemIndex); + break; + } + + if (objectName.empty()) + return; + + std::vector names; + names.push_back(objectName); + + m_ApiClient->GetObjects(type->PluralName, boost::bind(&MainForm::ObjectDetailsCompletionHandler, this, _1, true), names); +} + +wxPGProperty *MainForm::ValueToProperty(const String& name, const Value& value) +{ + wxPGProperty *prop; + + if (value.IsNumber()) { + double val = value; + return new wxFloatProperty(name.GetData(), wxPG_LABEL, value); + } else if (value.IsBoolean()) { + bool val = value; + return new wxBoolProperty(name.GetData(), wxPG_LABEL, value); + } else if (value.IsObjectType()) { + wxArrayString val; + Array::Ptr arr = value; + ObjectLock olock(arr); + BOOST_FOREACH(const Value& aitem, arr) + { + String val1 = aitem; + val.Add(val1.GetData()); + } + + return new wxArrayStringProperty(name.GetData(), wxPG_LABEL, val); + } else if (value.IsObjectType()) { + wxStringProperty *prop = new wxStringProperty(name.GetData(), wxPG_LABEL, ""); + + Dictionary::Ptr dict = value; + ObjectLock olock(dict); + BOOST_FOREACH(const Dictionary::Pair& kv, dict) { + prop->AppendChild(ValueToProperty(kv.first, kv.second)); + } + + return prop; + } else { + String val = value; + return new wxStringProperty(name.GetData(), wxPG_LABEL, val.GetData()); + } +} + +void MainForm::ObjectDetailsCompletionHandler(const std::vector& objects, bool forward) +{ + if (forward) { + CallAfter(boost::bind(&MainForm::ObjectDetailsCompletionHandler, this, objects, false)); + return; + } + + wxTreeItemId selectedId = m_TypesTree->GetSelection(); + wxString typeName = m_TypesTree->GetItemText(selectedId); + ApiType::Ptr type = m_Types[typeName.ToStdString()]; + + String nameAttr = type->Name.ToLower() + ".__name"; + + m_PropertyGrid->Clear(); + + if (objects.empty()) + return; + + ApiObject::Ptr object = objects[0]; + + typedef std::pair kv_pair; + BOOST_FOREACH(const kv_pair& kv, object->Attrs) { + std::vector tokens; + boost::algorithm::split(tokens, kv.first, boost::is_any_of(".")); + + wxPGProperty *prop = ValueToProperty(tokens[1], kv.second); + m_PropertyGrid->Append(prop); + m_PropertyGrid->SetPropertyReadOnly(prop); + } +} + +void MainForm::OnQuitClicked(wxCommandEvent& event) +{ + Close(); +} + +void MainForm::OnAboutClicked(wxCommandEvent& event) +{ + AboutForm form(this); + form.ShowModal(); +} diff --git a/icinga-studio/mainform.hpp b/icinga-studio/mainform.hpp new file mode 100644 index 000000000..a961edba0 --- /dev/null +++ b/icinga-studio/mainform.hpp @@ -0,0 +1,53 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#ifndef MAINFORM_H +#define MAINFORM_H + +#include "icinga-studio/api.hpp" +#include "remote/url.hpp" +#include "icinga-studio/forms.h" + +namespace icinga +{ + +class MainForm : public MainFormBase +{ +public: + MainForm(wxWindow *parent, const Url::Ptr& url); + + virtual void OnQuitClicked(wxCommandEvent& event) override; + virtual void OnAboutClicked(wxCommandEvent& event) override; + virtual void OnTypeSelected(wxTreeEvent& event) override; + virtual void OnObjectSelected(wxListEvent& event) override; + +private: + ApiClient::Ptr m_ApiClient; + std::map m_Types; + + void TypesCompletionHandler(const std::vector& types, bool forward); + void ObjectsCompletionHandler(const std::vector& objects, bool forward); + void ObjectDetailsCompletionHandler(const std::vector& objects, bool forward); + + wxPGProperty *ValueToProperty(const String& name, const Value& value); +}; + +} + +#endif /* MAINFORM_H */ \ No newline at end of file diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt index 2754ea39a..0adaa2fbd 100644 --- a/lib/base/CMakeLists.txt +++ b/lib/base/CMakeLists.txt @@ -86,4 +86,11 @@ install( LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/icinga2 ) +if(APPLE) + install( + TARGETS base + LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}/icinga-studio.app/Contents + ) +endif() + set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}" PARENT_SCOPE) diff --git a/lib/base/exception.hpp b/lib/base/exception.hpp index 7ceec1ff3..90e219239 100644 --- a/lib/base/exception.hpp +++ b/lib/base/exception.hpp @@ -147,7 +147,15 @@ typedef boost::error_info errinfo_getadd inline std::string to_string(const errinfo_getaddrinfo_error& e) { - return "[errinfo_getaddrinfo_error] = " + String(gai_strerror(e.value())) + "\n"; + String msg; + +#ifdef _WIN32 + msg = gai_strerrorA(e.value()); +#else /* _WIN32 */ + msg = gai_strerror(e.value()); +#endif /* _WIN32 */ + + return "[errinfo_getaddrinfo_error] = " + String(msg) + "\n"; } struct errinfo_message_; diff --git a/lib/base/socketevents.cpp b/lib/base/socketevents.cpp index 045563ba2..8c420e709 100644 --- a/lib/base/socketevents.cpp +++ b/lib/base/socketevents.cpp @@ -207,6 +207,8 @@ void SocketEvents::Register(Object *lifesupportObject) l_SocketIOSockets[m_FD] = desc; + m_Events = true; + /* There's no need to wake up the I/O thread here. */ } @@ -220,6 +222,8 @@ void SocketEvents::Unregister(void) l_SocketIOSockets.erase(m_FD); m_FD = INVALID_SOCKET; + + m_Events = false; } WakeUpThread(true); @@ -244,6 +248,12 @@ void SocketEvents::ChangeEvents(int events) WakeUpThread(); } +bool SocketEvents::IsHandlingEvents(void) const +{ + boost::mutex::scoped_lock lock(l_SocketIOMutex); + return m_Events; +} + void SocketEvents::OnEvent(int revents) { diff --git a/lib/base/socketevents.hpp b/lib/base/socketevents.hpp index 2523d6543..82a565727 100644 --- a/lib/base/socketevents.hpp +++ b/lib/base/socketevents.hpp @@ -42,11 +42,14 @@ public: void ChangeEvents(int events); + bool IsHandlingEvents(void) const; + protected: SocketEvents(const Socket::Ptr& socket, Object *lifesupportObject); private: SOCKET m_FD; + bool m_Events; static void InitializeThread(void); static void ThreadProc(void); diff --git a/lib/base/tlsstream.cpp b/lib/base/tlsstream.cpp index 2430ef29a..9bcb5c6eb 100644 --- a/lib/base/tlsstream.cpp +++ b/lib/base/tlsstream.cpp @@ -191,7 +191,7 @@ void TlsStream::OnEvent(int revents) lock.unlock(); - while (m_RecvQ->IsDataAvailable()) + while (m_RecvQ->IsDataAvailable() && IsHandlingEvents()) SignalDataAvailable(); if (m_Shutdown && !m_SendQ->IsDataAvailable()) @@ -318,6 +318,8 @@ void TlsStream::Close(void) boost::mutex::scoped_lock lock(m_Mutex); + m_Eof = true; + if (!m_SSL) return; @@ -326,8 +328,6 @@ void TlsStream::Close(void) m_Socket->Close(); m_Socket.reset(); - - m_Eof = true; } bool TlsStream::IsEof(void) const diff --git a/lib/base/tlsstream.hpp b/lib/base/tlsstream.hpp index e0e90a57c..669e67c7b 100644 --- a/lib/base/tlsstream.hpp +++ b/lib/base/tlsstream.hpp @@ -48,7 +48,7 @@ class I2_BASE_API TlsStream : public Stream, private SocketEvents public: DECLARE_PTR_TYPEDEFS(TlsStream); - TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const boost::shared_ptr& sslContext); + TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const boost::shared_ptr& sslContext = MakeSSLContext()); ~TlsStream(void); boost::shared_ptr GetClientCertificate(void) const; diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index 37f3328ce..eccc343fe 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -88,30 +88,34 @@ boost::shared_ptr MakeSSLContext(const String& pubkey, const String& pr SSL_CTX_set_mode(sslContext.get(), SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); SSL_CTX_set_session_id_context(sslContext.get(), (const unsigned char *)"Icinga 2", 8); - if (!SSL_CTX_use_certificate_chain_file(sslContext.get(), pubkey.CStr())) { - Log(LogCritical, "SSL") - << "Error with public key file '" << pubkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - BOOST_THROW_EXCEPTION(openssl_error() - << boost::errinfo_api_function("SSL_CTX_use_certificate_chain_file") - << errinfo_openssl_error(ERR_peek_error()) - << boost::errinfo_file_name(pubkey)); + if (!pubkey.IsEmpty()) { + if (!SSL_CTX_use_certificate_chain_file(sslContext.get(), pubkey.CStr())) { + Log(LogCritical, "SSL") + << "Error with public key file '" << pubkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; + BOOST_THROW_EXCEPTION(openssl_error() + << boost::errinfo_api_function("SSL_CTX_use_certificate_chain_file") + << errinfo_openssl_error(ERR_peek_error()) + << boost::errinfo_file_name(pubkey)); + } } - if (!SSL_CTX_use_PrivateKey_file(sslContext.get(), privkey.CStr(), SSL_FILETYPE_PEM)) { - Log(LogCritical, "SSL") - << "Error with private key file '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - BOOST_THROW_EXCEPTION(openssl_error() - << boost::errinfo_api_function("SSL_CTX_use_PrivateKey_file") - << errinfo_openssl_error(ERR_peek_error()) - << boost::errinfo_file_name(privkey)); - } + if (!privkey.IsEmpty()) { + if (!SSL_CTX_use_PrivateKey_file(sslContext.get(), privkey.CStr(), SSL_FILETYPE_PEM)) { + Log(LogCritical, "SSL") + << "Error with private key file '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; + BOOST_THROW_EXCEPTION(openssl_error() + << boost::errinfo_api_function("SSL_CTX_use_PrivateKey_file") + << errinfo_openssl_error(ERR_peek_error()) + << boost::errinfo_file_name(privkey)); + } - if (!SSL_CTX_check_private_key(sslContext.get())) { - Log(LogCritical, "SSL") - << "Error checking private key '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; - BOOST_THROW_EXCEPTION(openssl_error() - << boost::errinfo_api_function("SSL_CTX_check_private_key") - << errinfo_openssl_error(ERR_peek_error())); + if (!SSL_CTX_check_private_key(sslContext.get())) { + Log(LogCritical, "SSL") + << "Error checking private key '" << privkey << "': " << ERR_peek_error() << ", \"" << ERR_error_string(ERR_peek_error(), errbuf) << "\""; + BOOST_THROW_EXCEPTION(openssl_error() + << boost::errinfo_api_function("SSL_CTX_check_private_key") + << errinfo_openssl_error(ERR_peek_error())); + } } if (!cakey.IsEmpty()) { diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index d994754c9..68129c3ec 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -38,7 +38,7 @@ namespace icinga { void I2_BASE_API InitializeOpenSSL(void); -boost::shared_ptr I2_BASE_API MakeSSLContext(const String& pubkey, const String& privkey, const String& cakey = String()); +boost::shared_ptr I2_BASE_API MakeSSLContext(const String& pubkey = String(), const String& privkey = String(), const String& cakey = String()); void I2_BASE_API AddCRLToSSLContext(const boost::shared_ptr& context, const String& crlPath); String I2_BASE_API GetCertificateCN(const boost::shared_ptr& certificate); boost::shared_ptr I2_BASE_API GetX509Certificate(const String& pemfile); diff --git a/lib/base/win32.hpp b/lib/base/win32.hpp index c121911f9..76d3efc41 100644 --- a/lib/base/win32.hpp +++ b/lib/base/win32.hpp @@ -21,10 +21,12 @@ #define WIN32_H #define WIN32_LEAN_AND_MEAN +#ifndef _WIN32_WINNT #define _WIN32_WINNT _WIN32_WINNT_VISTA +#endif /* _WIN32_WINNT */ #define NOMINMAX -#include #include +#include #include #include #include diff --git a/lib/config/CMakeLists.txt b/lib/config/CMakeLists.txt index f60b1b852..048250cf4 100644 --- a/lib/config/CMakeLists.txt +++ b/lib/config/CMakeLists.txt @@ -61,3 +61,10 @@ install( RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/icinga2 ) + +if(APPLE) + install( + TARGETS config + LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}/icinga-studio.app/Contents + ) +endif() diff --git a/lib/remote/CMakeLists.txt b/lib/remote/CMakeLists.txt index 0d35f445c..a728a4aa6 100644 --- a/lib/remote/CMakeLists.txt +++ b/lib/remote/CMakeLists.txt @@ -27,7 +27,7 @@ set(remote_SOURCES configfileshandler.cpp configmoduleshandler.cpp configmoduleutility.cpp configobjectutility.cpp configstageshandler.cpp createobjecthandler.cpp deleteobjecthandler.cpp endpoint.cpp endpoint.thpp filterutility.cpp - httpchunkedencoding.cpp httpconnection.cpp httphandler.cpp httprequest.cpp httpresponse.cpp + httpchunkedencoding.cpp httpclientconnection.cpp httpserverconnection.cpp httphandler.cpp httprequest.cpp httpresponse.cpp httputility.cpp jsonrpc.cpp jsonrpcconnection.cpp jsonrpcconnection-heartbeat.cpp messageorigin.cpp modifyobjecthandler.cpp statusqueryhandler.cpp typequeryhandler.cpp url.cpp zone.cpp zone.thpp @@ -55,6 +55,13 @@ install( LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/icinga2 ) +if(APPLE) + install( + TARGETS remote + LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}/icinga-studio.app/Contents + ) +endif() + #install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/lib/icinga2/api\")") install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/lib/icinga2/api/log\")") install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/lib/icinga2/api/repository\")") diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index 3c27a38c5..ed53f2478 100644 --- a/lib/remote/apilistener.cpp +++ b/lib/remote/apilistener.cpp @@ -386,7 +386,7 @@ void ApiListener::NewClientHandlerInternal(const Socket::Ptr& client, const Stri } else { Log(LogInformation, "ApiListener", "New HTTP client"); - HttpConnection::Ptr aclient = new HttpConnection(identity, verify_ok, tlsStream); + HttpServerConnection::Ptr aclient = new HttpServerConnection(identity, verify_ok, tlsStream); aclient->Start(); AddHttpClient(aclient); } @@ -908,19 +908,19 @@ std::set ApiListener::GetAnonymousClients(void) const return m_AnonymousClients; } -void ApiListener::AddHttpClient(const HttpConnection::Ptr& aclient) +void ApiListener::AddHttpClient(const HttpServerConnection::Ptr& aclient) { ObjectLock olock(this); m_HttpClients.insert(aclient); } -void ApiListener::RemoveHttpClient(const HttpConnection::Ptr& aclient) +void ApiListener::RemoveHttpClient(const HttpServerConnection::Ptr& aclient) { ObjectLock olock(this); m_HttpClients.erase(aclient); } -std::set ApiListener::GetHttpClients(void) const +std::set ApiListener::GetHttpClients(void) const { ObjectLock olock(this); return m_HttpClients; diff --git a/lib/remote/apilistener.hpp b/lib/remote/apilistener.hpp index 03559a1e1..4990b4101 100644 --- a/lib/remote/apilistener.hpp +++ b/lib/remote/apilistener.hpp @@ -22,7 +22,7 @@ #include "remote/apilistener.thpp" #include "remote/jsonrpcconnection.hpp" -#include "remote/httpconnection.hpp" +#include "remote/httpserverconnection.hpp" #include "remote/endpoint.hpp" #include "remote/messageorigin.hpp" #include "base/configobject.hpp" @@ -69,9 +69,9 @@ public: void RemoveAnonymousClient(const JsonRpcConnection::Ptr& aclient); std::set GetAnonymousClients(void) const; - void AddHttpClient(const HttpConnection::Ptr& aclient); - void RemoveHttpClient(const HttpConnection::Ptr& aclient); - std::set GetHttpClients(void) const; + void AddHttpClient(const HttpServerConnection::Ptr& aclient); + void RemoveHttpClient(const HttpServerConnection::Ptr& aclient); + std::set GetHttpClients(void) const; static Value ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params); @@ -85,7 +85,7 @@ private: boost::shared_ptr m_SSLContext; std::set m_Servers; std::set m_AnonymousClients; - std::set m_HttpClients; + std::set m_HttpClients; Timer::Ptr m_Timer; void ApiTimerHandler(void); diff --git a/lib/remote/httpchunkedencoding.cpp b/lib/remote/httpchunkedencoding.cpp index 4d8aa7ca8..5fd3bc8fd 100644 --- a/lib/remote/httpchunkedencoding.cpp +++ b/lib/remote/httpchunkedencoding.cpp @@ -28,6 +28,7 @@ StreamReadStatus HttpChunkedEncoding::ReadChunkFromStream(const Stream::Ptr& str if (context.LengthIndicator == -1) { String line; StreamReadStatus status = stream->ReadLine(&line, context.StreamContext, may_wait); + may_wait = false; if (status != StatusNewItem) return status; @@ -36,35 +37,36 @@ StreamReadStatus HttpChunkedEncoding::ReadChunkFromStream(const Stream::Ptr& str msgbuf << std::hex << line; msgbuf >> context.LengthIndicator; - return StatusNeedData; - } else { - StreamReadContext& scontext = context.StreamContext; - if (scontext.Eof) - return StatusEof; - - if (scontext.MustRead) { - if (!scontext.FillFromStream(stream, may_wait)) { - scontext.Eof = true; - return StatusEof; - } + } - scontext.MustRead = false; - } + StreamReadContext& scontext = context.StreamContext; + if (scontext.Eof) + return StatusEof; - if (scontext.Size < (size_t)context.LengthIndicator) { - scontext.MustRead = true; - return StatusNeedData; + if (scontext.MustRead) { + if (!scontext.FillFromStream(stream, may_wait)) { + scontext.Eof = true; + return StatusEof; } - *data = new char[context.LengthIndicator]; - *size = context.LengthIndicator; - memcpy(data, scontext.Buffer, context.LengthIndicator); + scontext.MustRead = false; + } - scontext.DropData(context.LengthIndicator); - context.LengthIndicator = -1; + size_t NewlineLength = context.LengthIndicator ? 2 : 0; - return StatusNewItem; + if (scontext.Size < (size_t)context.LengthIndicator + NewlineLength) { + scontext.MustRead = true; + return StatusNeedData; } + + *data = new char[context.LengthIndicator]; + *size = context.LengthIndicator; + memcpy(*data, scontext.Buffer, context.LengthIndicator); + + scontext.DropData(context.LengthIndicator + NewlineLength); + context.LengthIndicator = -1; + + return StatusNewItem; } void HttpChunkedEncoding::WriteChunkToStream(const Stream::Ptr& stream, const char *data, size_t count) diff --git a/lib/remote/httpclientconnection.cpp b/lib/remote/httpclientconnection.cpp new file mode 100644 index 000000000..4837ed3ec --- /dev/null +++ b/lib/remote/httpclientconnection.cpp @@ -0,0 +1,156 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#include "remote/httpclientconnection.hpp" +#include "remote/base64.hpp" +#include "base/configtype.hpp" +#include "base/objectlock.hpp" +#include "base/utility.hpp" +#include "base/logger.hpp" +#include "base/exception.hpp" +#include "base/convert.hpp" +#include "base/tcpsocket.hpp" +#include "base/tlsstream.hpp" +#include "base/networkstream.hpp" +#include + +using namespace icinga; + +HttpClientConnection::HttpClientConnection(const String& host, const String& port, bool tls) + : m_Host(host), m_Port(port), m_Tls(tls) +{ } + +void HttpClientConnection::Start(void) +{ + /* Nothing to do here atm. */ +} + +void HttpClientConnection::Reconnect(void) +{ + if (m_Stream) + m_Stream->Close(); + + m_Context.~StreamReadContext(); + new (&m_Context) StreamReadContext(); + + TcpSocket::Ptr socket = new TcpSocket(); + socket->Connect(m_Host, m_Port); + + if (m_Tls) + m_Stream = new TlsStream(socket, m_Host, RoleClient); + else + ASSERT(!"Non-TLS HTTP connections not supported."); + //m_Stream = new NetworkStream(socket); -- does not currently work because the NetworkStream class doesn't support async I/O + + m_Stream->RegisterDataHandler(boost::bind(&HttpClientConnection::DataAvailableHandler, this)); + if (m_Stream->IsDataAvailable()) + DataAvailableHandler(); +} + +Stream::Ptr HttpClientConnection::GetStream(void) const +{ + return m_Stream; +} + +String HttpClientConnection::GetHost(void) const +{ + return m_Host; +} + +String HttpClientConnection::GetPort(void) const +{ + return m_Port; +} + +bool HttpClientConnection::GetTls(void) const +{ + return m_Tls; +} + +void HttpClientConnection::Disconnect(void) +{ + Log(LogDebug, "HttpClientConnection", "Http client disconnected"); + + m_Stream->Shutdown(); +} + +bool HttpClientConnection::ProcessMessage(void) +{ + bool res; + + if (m_Requests.empty()) + return false; + + const std::pair, HttpCompletionCallback>& currentRequest = *m_Requests.begin(); + HttpRequest& request = *currentRequest.first.get(); + const HttpCompletionCallback& callback = currentRequest.second; + + if (!m_CurrentResponse) + m_CurrentResponse = boost::make_shared(m_Stream, request); + + boost::shared_ptr currentResponse = m_CurrentResponse; + HttpResponse& response = *currentResponse.get(); + + try { + res = response.Parse(m_Context, false); + } catch (const std::exception& ex) { + callback(request, response); + + m_Stream->Shutdown(); + return false; + } + + if (response.Complete) { + callback(request, response); + + m_Requests.pop_front(); + m_CurrentResponse.reset(); + + return true; + } + + return res; +} + +void HttpClientConnection::DataAvailableHandler(void) +{ + boost::mutex::scoped_lock lock(m_DataHandlerMutex); + + try { + while (ProcessMessage()) + ; /* empty loop body */ + } catch (const std::exception& ex) { + Log(LogWarning, "HttpClientConnection") + << "Error while reading Http request: " << DiagnosticInformation(ex); + + Disconnect(); + } +} + +boost::shared_ptr HttpClientConnection::NewRequest(void) +{ + Reconnect(); + return boost::make_shared(m_Stream); +} + +void HttpClientConnection::SubmitRequest(const boost::shared_ptr& request, const HttpCompletionCallback& callback) +{ + m_Requests.push_back(std::make_pair(request, callback)); + request->Finish(); +} diff --git a/lib/remote/httpclientconnection.hpp b/lib/remote/httpclientconnection.hpp new file mode 100644 index 000000000..72fcfeef9 --- /dev/null +++ b/lib/remote/httpclientconnection.hpp @@ -0,0 +1,78 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#ifndef HTTPCLIENTCONNECTION_H +#define HTTPCLIENTCONNECTION_H + +#include "remote/httprequest.hpp" +#include "remote/httpresponse.hpp" +#include "base/stream.hpp" +#include "base/timer.hpp" +#include + +namespace icinga +{ + +/** + * An HTTP client connection. + * + * @ingroup remote + */ +class I2_REMOTE_API HttpClientConnection : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(HttpClientConnection); + + HttpClientConnection(const String& host, const String& port, bool tls = true); + + void Start(void); + + Stream::Ptr GetStream(void) const; + String GetHost(void) const; + String GetPort(void) const; + bool GetTls(void) const; + + void Disconnect(void); + + boost::shared_ptr NewRequest(void); + + typedef boost::function HttpCompletionCallback; + void SubmitRequest(const boost::shared_ptr& request, const HttpCompletionCallback& callback); + +private: + String m_Host; + String m_Port; + bool m_Tls; + Stream::Ptr m_Stream; + std::deque, HttpCompletionCallback> > m_Requests; + boost::shared_ptr m_CurrentResponse; + boost::mutex m_DataHandlerMutex; + + StreamReadContext m_Context; + + void Reconnect(void); + bool ProcessMessage(void); + void DataAvailableHandler(void); + + void ProcessMessageAsync(HttpRequest& request); +}; + +} + +#endif /* HTTPCLIENTCONNECTION_H */ diff --git a/lib/remote/httprequest.cpp b/lib/remote/httprequest.cpp index 951169e9d..28634563f 100644 --- a/lib/remote/httprequest.cpp +++ b/lib/remote/httprequest.cpp @@ -19,27 +19,29 @@ #include "remote/httprequest.hpp" #include "base/logger.hpp" +#include "base/application.hpp" #include "base/convert.hpp" #include #include #include +#include +#include using namespace icinga; -HttpRequest::HttpRequest(StreamReadContext& src) +HttpRequest::HttpRequest(const Stream::Ptr& stream) : Complete(false), - ProtocolVersion(HttpVersion10), + ProtocolVersion(HttpVersion11), Headers(new Dictionary()), - m_Context(src), - m_ChunkContext(m_Context), + m_Stream(stream), m_State(HttpRequestStart) { } -bool HttpRequest::Parse(const Stream::Ptr& stream, StreamReadContext& src, bool may_wait) +bool HttpRequest::Parse(StreamReadContext& src, bool may_wait) { if (m_State != HttpRequestBody) { String line; - StreamReadStatus srs = stream->ReadLine(&line, src, may_wait); + StreamReadStatus srs = m_Stream->ReadLine(&line, src, may_wait); if (srs != StatusNewItem) return false; @@ -95,9 +97,12 @@ bool HttpRequest::Parse(const Stream::Ptr& stream, StreamReadContext& src, bool } } else if (m_State == HttpRequestBody) { if (Headers->Get("transfer-encoding") == "chunked") { + if (!m_ChunkContext) + m_ChunkContext = boost::make_shared(src); + char *data; size_t size; - StreamReadStatus srs = HttpChunkedEncoding::ReadChunkFromStream(stream, &data, &size, m_ChunkContext, false); + StreamReadStatus srs = HttpChunkedEncoding::ReadChunkFromStream(m_Stream, &data, &size, *m_ChunkContext.get(), may_wait); if (srs != StatusNewItem) return false; @@ -114,27 +119,27 @@ bool HttpRequest::Parse(const Stream::Ptr& stream, StreamReadContext& src, bool return true; } } else { - if (m_Context.Eof) + if (src.Eof) BOOST_THROW_EXCEPTION(std::invalid_argument("Unexpected EOF in HTTP body")); - if (m_Context.MustRead) { - if (!m_Context.FillFromStream(stream, false)) { - m_Context.Eof = true; + if (src.MustRead) { + if (!src.FillFromStream(m_Stream, false)) { + src.Eof = true; BOOST_THROW_EXCEPTION(std::invalid_argument("Unexpected EOF in HTTP body")); } - m_Context.MustRead = false; + src.MustRead = false; } size_t length_indicator = Convert::ToLong(Headers->Get("content-length")); - if (m_Context.Size < length_indicator) { - m_Context.MustRead = true; + if (src.Size < length_indicator) { + src.MustRead = true; return false; } - m_Body->Write(m_Context.Buffer, length_indicator); - m_Context.DropData(length_indicator); + m_Body->Write(src.Buffer, length_indicator); + src.DropData(length_indicator); Complete = true; return true; } @@ -151,3 +156,77 @@ size_t HttpRequest::ReadBody(char *data, size_t count) return m_Body->Read(data, count, true); } +void HttpRequest::AddHeader(const String& key, const String& value) +{ + ASSERT(m_State == HttpRequestStart || m_State == HttpRequestHeaders); + Headers->Set(key.ToLower(), value); +} + +void HttpRequest::FinishHeaders(void) +{ + if (m_State == HttpRequestStart) { + String rqline = RequestMethod + " " + RequestUrl->Format() + " HTTP/1." + (ProtocolVersion == HttpVersion10 ? "0" : "1") + "\n"; + m_Stream->Write(rqline.CStr(), rqline.GetLength()); + m_State = HttpRequestHeaders; + } + + if (m_State == HttpRequestHeaders) { + AddHeader("User-Agent", "Icinga/" + Application::GetVersion()); + + if (ProtocolVersion == HttpVersion11) + AddHeader("Transfer-Encoding", "chunked"); + + ObjectLock olock(Headers); + BOOST_FOREACH(const Dictionary::Pair& kv, Headers) + { + String header = kv.first + ": " + kv.second + "\n"; + m_Stream->Write(header.CStr(), header.GetLength()); + } + + m_Stream->Write("\n", 1); + + m_State = HttpRequestBody; + } +} + +void HttpRequest::WriteBody(const char *data, size_t count) +{ + ASSERT(m_State == HttpRequestStart || m_State == HttpRequestHeaders || m_State == HttpRequestBody); + + if (ProtocolVersion == HttpVersion10) { + if (!m_Body) + m_Body = new FIFO(); + + m_Body->Write(data, count); + } else { + FinishHeaders(); + + HttpChunkedEncoding::WriteChunkToStream(m_Stream, data, count); + } +} + +void HttpRequest::Finish(void) +{ + ASSERT(m_State != HttpRequestEnd); + + if (ProtocolVersion == HttpVersion10) { + if (m_Body) + AddHeader("Content-Length", Convert::ToString(m_Body->GetAvailableBytes())); + + FinishHeaders(); + + while (m_Body && m_Body->IsDataAvailable()) { + char buffer[1024]; + size_t rc = m_Body->Read(buffer, sizeof(buffer), true); + m_Stream->Write(buffer, rc); + } + } else { + if (m_State == HttpRequestStart || m_State == HttpRequestHeaders) + FinishHeaders(); + + WriteBody(NULL, 0); + m_Stream->Write("\r\n", 2); + } + + m_State = HttpRequestEnd; +} diff --git a/lib/remote/httprequest.hpp b/lib/remote/httprequest.hpp index b04dfd09c..9eb1a0b21 100644 --- a/lib/remote/httprequest.hpp +++ b/lib/remote/httprequest.hpp @@ -40,7 +40,8 @@ enum HttpRequestState { HttpRequestStart, HttpRequestHeaders, - HttpRequestBody + HttpRequestBody, + HttpRequestEnd }; /** @@ -59,17 +60,22 @@ public: Dictionary::Ptr Headers; - HttpRequest(StreamReadContext& ctx); - - bool Parse(const Stream::Ptr& stream, StreamReadContext& src, bool may_wait); + HttpRequest(const Stream::Ptr& stream); + bool Parse(StreamReadContext& src, bool may_wait); size_t ReadBody(char *data, size_t count); + void AddHeader(const String& key, const String& value); + void WriteBody(const char *data, size_t count); + void Finish(void); + private: - StreamReadContext& m_Context; - ChunkReadContext m_ChunkContext; + Stream::Ptr m_Stream; + boost::shared_ptr m_ChunkContext; HttpRequestState m_State; FIFO::Ptr m_Body; + + void FinishHeaders(void); }; } diff --git a/lib/remote/httpresponse.cpp b/lib/remote/httpresponse.cpp index 631c13a35..fdd5d663d 100644 --- a/lib/remote/httpresponse.cpp +++ b/lib/remote/httpresponse.cpp @@ -20,13 +20,16 @@ #include "remote/httpresponse.hpp" #include "remote/httpchunkedencoding.hpp" #include "base/logger.hpp" +#include +#include #include "base/application.hpp" #include "base/convert.hpp" +#include using namespace icinga; HttpResponse::HttpResponse(const Stream::Ptr& stream, const HttpRequest& request) - : m_State(HttpResponseStart), m_Request(request), m_Stream(stream) + : Complete(false), m_State(HttpResponseStart), m_Request(request), m_Stream(stream) { } void HttpResponse::SetStatus(int code, const String& message) @@ -109,3 +112,123 @@ void HttpResponse::Finish(void) if (m_Request.ProtocolVersion == HttpVersion10 || m_Request.Headers->Get("connection") == "close") m_Stream->Shutdown(); } + +bool HttpResponse::Parse(StreamReadContext& src, bool may_wait) +{ + if (m_State != HttpResponseBody) { + String line; + StreamReadStatus srs = m_Stream->ReadLine(&line, src, may_wait); + + if (srs != StatusNewItem) + return false; + + if (m_State == HttpResponseStart) { + /* ignore trailing new-lines */ + if (line == "") + return true; + + std::vector tokens; + boost::algorithm::split(tokens, line, boost::is_any_of(" ")); + Log(LogDebug, "HttpRequest") + << "line: " << line << ", tokens: " << tokens.size(); + if (tokens.size() < 3) + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid HTTP request")); + + if (tokens[0] == "HTTP/1.0") + ProtocolVersion = HttpVersion10; + else if (tokens[0] == "HTTP/1.1") { + ProtocolVersion = HttpVersion11; + } else + BOOST_THROW_EXCEPTION(std::invalid_argument("Unsupported HTTP version")); + + StatusCode = Convert::ToLong(tokens[1]); + StatusMessage = tokens[2]; // TODO: Join tokens[2..end] + + m_State = HttpResponseHeaders; + } else if (m_State == HttpResponseHeaders) { + if (!Headers) + Headers = new Dictionary(); + + if (line == "") { + m_State = HttpResponseBody; + + /* we're done if the request doesn't contain a message body */ + if (!Headers->Contains("content-length") && !Headers->Contains("transfer-encoding")) + Complete = true; + else + m_Body = new FIFO(); + + return true; + + } else { + String::SizeType pos = line.FindFirstOf(":"); + if (pos == String::NPos) + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid HTTP request")); + String key = line.SubStr(0, pos).ToLower().Trim(); + + String value = line.SubStr(pos + 1).Trim(); + Headers->Set(key, value); + } + } else { + VERIFY(!"Invalid HTTP request state."); + } + } else if (m_State == HttpResponseBody) { + if (Headers->Get("transfer-encoding") == "chunked") { + if (!m_ChunkContext) + m_ChunkContext = boost::make_shared(src); + + char *data; + size_t size; + StreamReadStatus srs = HttpChunkedEncoding::ReadChunkFromStream(m_Stream, &data, &size, *m_ChunkContext.get(), may_wait); + + if (srs != StatusNewItem) + return false; + + Log(LogInformation, "HttpResponse") + << "Read " << size << " bytes"; + + m_Body->Write(data, size); + + delete[] data; + + if (size == 0) { + Complete = true; + return true; + } + } else { + if (src.Eof) + BOOST_THROW_EXCEPTION(std::invalid_argument("Unexpected EOF in HTTP body")); + + if (src.MustRead) { + if (!src.FillFromStream(m_Stream, false)) { + src.Eof = true; + BOOST_THROW_EXCEPTION(std::invalid_argument("Unexpected EOF in HTTP body")); + } + + src.MustRead = false; + } + + size_t length_indicator = Convert::ToLong(Headers->Get("content-length")); + + if (src.Size < length_indicator) { + src.MustRead = true; + return false; + } + + m_Body->Write(src.Buffer, length_indicator); + src.DropData(length_indicator); + Complete = true; + return true; + } + } + + return true; +} + +size_t HttpResponse::ReadBody(char *data, size_t count) +{ + if (!m_Body) + return 0; + else + return m_Body->Read(data, count, true); +} \ No newline at end of file diff --git a/lib/remote/httpresponse.hpp b/lib/remote/httpresponse.hpp index 6255ac134..94c11d3f0 100644 --- a/lib/remote/httpresponse.hpp +++ b/lib/remote/httpresponse.hpp @@ -43,8 +43,19 @@ enum HttpResponseState struct I2_REMOTE_API HttpResponse { public: + bool Complete; + + HttpVersion ProtocolVersion; + int StatusCode; + String StatusMessage; + + Dictionary::Ptr Headers; + HttpResponse(const Stream::Ptr& stream, const HttpRequest& request); + bool Parse(StreamReadContext& src, bool may_wait); + size_t ReadBody(char *data, size_t count); + void SetStatus(int code, const String& message); void AddHeader(const String& key, const String& value); void WriteBody(const char *data, size_t count); @@ -52,6 +63,7 @@ public: private: HttpResponseState m_State; + boost::shared_ptr m_ChunkContext; const HttpRequest& m_Request; Stream::Ptr m_Stream; FIFO::Ptr m_Body; diff --git a/lib/remote/httpconnection.cpp b/lib/remote/httpserverconnection.cpp similarity index 70% rename from lib/remote/httpconnection.cpp rename to lib/remote/httpserverconnection.cpp index 48c1ac663..e97d9c2c7 100644 --- a/lib/remote/httpconnection.cpp +++ b/lib/remote/httpserverconnection.cpp @@ -17,7 +17,7 @@ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * ******************************************************************************/ -#include "remote/httpconnection.hpp" +#include "remote/httpserverconnection.hpp" #include "remote/httphandler.hpp" #include "remote/apilistener.hpp" #include "remote/apifunction.hpp" @@ -33,47 +33,46 @@ using namespace icinga; -static boost::once_flag l_HttpConnectionOnceFlag = BOOST_ONCE_INIT; -static Timer::Ptr l_HttpConnectionTimeoutTimer; +static boost::once_flag l_HttpServerConnectionOnceFlag = BOOST_ONCE_INIT; +static Timer::Ptr l_HttpServerConnectionTimeoutTimer; -HttpConnection::HttpConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream) - : m_Stream(stream), m_Seen(Utility::GetTime()), - m_CurrentRequest(m_Context), m_PendingRequests(0) +HttpServerConnection::HttpServerConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream) + : m_Stream(stream), m_CurrentRequest(stream), m_Seen(Utility::GetTime()), m_PendingRequests(0) { - boost::call_once(l_HttpConnectionOnceFlag, &HttpConnection::StaticInitialize); + boost::call_once(l_HttpServerConnectionOnceFlag, &HttpServerConnection::StaticInitialize); if (authenticated) m_ApiUser = ApiUser::GetByClientCN(identity); } -void HttpConnection::StaticInitialize(void) +void HttpServerConnection::StaticInitialize(void) { - l_HttpConnectionTimeoutTimer = new Timer(); - l_HttpConnectionTimeoutTimer->OnTimerExpired.connect(boost::bind(&HttpConnection::TimeoutTimerHandler)); - l_HttpConnectionTimeoutTimer->SetInterval(15); - l_HttpConnectionTimeoutTimer->Start(); + l_HttpServerConnectionTimeoutTimer = new Timer(); + l_HttpServerConnectionTimeoutTimer->OnTimerExpired.connect(boost::bind(&HttpServerConnection::TimeoutTimerHandler)); + l_HttpServerConnectionTimeoutTimer->SetInterval(15); + l_HttpServerConnectionTimeoutTimer->Start(); } -void HttpConnection::Start(void) +void HttpServerConnection::Start(void) { - m_Stream->RegisterDataHandler(boost::bind(&HttpConnection::DataAvailableHandler, this)); + m_Stream->RegisterDataHandler(boost::bind(&HttpServerConnection::DataAvailableHandler, this)); if (m_Stream->IsDataAvailable()) DataAvailableHandler(); } -ApiUser::Ptr HttpConnection::GetApiUser(void) const +ApiUser::Ptr HttpServerConnection::GetApiUser(void) const { return m_ApiUser; } -TlsStream::Ptr HttpConnection::GetStream(void) const +TlsStream::Ptr HttpServerConnection::GetStream(void) const { return m_Stream; } -void HttpConnection::Disconnect(void) +void HttpServerConnection::Disconnect(void) { - Log(LogDebug, "HttpConnection", "Http client disconnected"); + Log(LogDebug, "HttpServerConnection", "Http client disconnected"); ApiListener::Ptr listener = ApiListener::GetInstance(); listener->RemoveHttpClient(this); @@ -81,12 +80,12 @@ void HttpConnection::Disconnect(void) m_Stream->Shutdown(); } -bool HttpConnection::ProcessMessage(void) +bool HttpServerConnection::ProcessMessage(void) { bool res; try { - res = m_CurrentRequest.Parse(m_Stream, m_Context, false); + res = m_CurrentRequest.Parse(m_Context, false); } catch (const std::exception& ex) { HttpResponse response(m_Stream, m_CurrentRequest); response.SetStatus(400, "Bad request"); @@ -99,13 +98,13 @@ bool HttpConnection::ProcessMessage(void) } if (m_CurrentRequest.Complete) { - m_RequestQueue.Enqueue(boost::bind(&HttpConnection::ProcessMessageAsync, HttpConnection::Ptr(this), m_CurrentRequest)); + m_RequestQueue.Enqueue(boost::bind(&HttpServerConnection::ProcessMessageAsync, HttpServerConnection::Ptr(this), m_CurrentRequest)); m_Seen = Utility::GetTime(); m_PendingRequests++; m_CurrentRequest.~HttpRequest(); - new (&m_CurrentRequest) HttpRequest(m_Context); + new (&m_CurrentRequest) HttpRequest(m_Stream); return true; } @@ -113,9 +112,9 @@ bool HttpConnection::ProcessMessage(void) return res; } -void HttpConnection::ProcessMessageAsync(HttpRequest& request) +void HttpServerConnection::ProcessMessageAsync(HttpRequest& request) { - Log(LogInformation, "HttpConnection", "Processing Http message"); + Log(LogInformation, "HttpServerConnection", "Processing Http message"); String auth_header = request.Headers->Get("authorization"); @@ -169,7 +168,7 @@ void HttpConnection::ProcessMessageAsync(HttpRequest& request) m_PendingRequests--; } -void HttpConnection::DataAvailableHandler(void) +void HttpServerConnection::DataAvailableHandler(void) { boost::mutex::scoped_lock lock(m_DataHandlerMutex); @@ -177,27 +176,27 @@ void HttpConnection::DataAvailableHandler(void) while (ProcessMessage()) ; /* empty loop body */ } catch (const std::exception& ex) { - Log(LogWarning, "HttpConnection") + Log(LogWarning, "HttpServerConnection") << "Error while reading Http request: " << DiagnosticInformation(ex); Disconnect(); } } -void HttpConnection::CheckLiveness(void) +void HttpServerConnection::CheckLiveness(void) { if (m_Seen < Utility::GetTime() - 10 && m_PendingRequests == 0) { - Log(LogInformation, "HttpConnection") + Log(LogInformation, "HttpServerConnection") << "No messages for Http connection have been received in the last 10 seconds."; Disconnect(); } } -void HttpConnection::TimeoutTimerHandler(void) +void HttpServerConnection::TimeoutTimerHandler(void) { ApiListener::Ptr listener = ApiListener::GetInstance(); - BOOST_FOREACH(const HttpConnection::Ptr& client, listener->GetHttpClients()) { + BOOST_FOREACH(const HttpServerConnection::Ptr& client, listener->GetHttpClients()) { client->CheckLiveness(); } } diff --git a/lib/remote/httpconnection.hpp b/lib/remote/httpserverconnection.hpp similarity index 88% rename from lib/remote/httpconnection.hpp rename to lib/remote/httpserverconnection.hpp index a1ca8cf7d..f684d5b6e 100644 --- a/lib/remote/httpconnection.hpp +++ b/lib/remote/httpserverconnection.hpp @@ -17,8 +17,8 @@ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * ******************************************************************************/ -#ifndef HTTPCONNECTION_H -#define HTTPCONNECTION_H +#ifndef HTTPSERVERCONNECTION_H +#define HTTPSERVERCONNECTION_H #include "remote/httprequest.hpp" #include "remote/apiuser.hpp" @@ -34,12 +34,12 @@ namespace icinga * * @ingroup remote */ -class I2_REMOTE_API HttpConnection : public Object +class I2_REMOTE_API HttpServerConnection : public Object { public: - DECLARE_PTR_TYPEDEFS(HttpConnection); + DECLARE_PTR_TYPEDEFS(HttpServerConnection); - HttpConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream); + HttpServerConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream); void Start(void); @@ -72,4 +72,4 @@ private: } -#endif /* HTTPCONNECTION_H */ +#endif /* HTTPSERVERCONNECTION_H */ diff --git a/third-party/execvpe/CMakeLists.txt b/third-party/execvpe/CMakeLists.txt index 4d5538006..47d07fe7c 100644 --- a/third-party/execvpe/CMakeLists.txt +++ b/third-party/execvpe/CMakeLists.txt @@ -29,3 +29,9 @@ install( LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/icinga2 ) +if(APPLE) + install( + TARGETS execvpe + LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}/icinga-studio.app/Contents + ) +endif() diff --git a/third-party/mmatch/CMakeLists.txt b/third-party/mmatch/CMakeLists.txt index 8298a0424..0cd1cc914 100644 --- a/third-party/mmatch/CMakeLists.txt +++ b/third-party/mmatch/CMakeLists.txt @@ -28,3 +28,10 @@ install( RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/icinga2 ) + +if(APPLE) + install( + TARGETS mmatch + LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}/icinga-studio.app/Contents + ) +endif() diff --git a/third-party/socketpair/CMakeLists.txt b/third-party/socketpair/CMakeLists.txt index 5dd113890..9c149cb50 100644 --- a/third-party/socketpair/CMakeLists.txt +++ b/third-party/socketpair/CMakeLists.txt @@ -33,3 +33,9 @@ install( LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/icinga2 ) +if(APPLE) + install( + TARGETS socketpair + LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}/icinga-studio.app/Contents + ) +endif() diff --git a/third-party/yajl/src/CMakeLists.txt b/third-party/yajl/src/CMakeLists.txt index 249b4783d..c5de3539d 100644 --- a/third-party/yajl/src/CMakeLists.txt +++ b/third-party/yajl/src/CMakeLists.txt @@ -60,3 +60,10 @@ INCLUDE_DIRECTORIES(${incDir}/..) INSTALL(TARGETS yajl RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/icinga2) + +if(APPLE) + install( + TARGETS yajl + LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}/icinga-studio.app/Contents + ) +endif()