diff --git a/lib/base/fifo.cpp b/lib/base/fifo.cpp index 688b5c69f..2a2f692f2 100644 --- a/lib/base/fifo.cpp +++ b/lib/base/fifo.cpp @@ -78,8 +78,10 @@ void FIFO::Optimize(void) } } -size_t FIFO::Peek(void *buffer, size_t count) +size_t FIFO::Peek(void *buffer, size_t count, bool allow_partial) { + ASSERT(allow_partial); + if (count > m_DataSize) count = m_DataSize; @@ -143,4 +145,4 @@ bool FIFO::SupportsWaiting(void) const bool FIFO::IsDataAvailable(void) const { return m_DataSize > 0; -} \ No newline at end of file +} diff --git a/lib/base/fifo.hpp b/lib/base/fifo.hpp index a2620ad82..84c3bb150 100644 --- a/lib/base/fifo.hpp +++ b/lib/base/fifo.hpp @@ -41,7 +41,7 @@ public: FIFO(void); ~FIFO(void); - size_t Peek(void *buffer, size_t count); + virtual size_t Peek(void *buffer, size_t count, bool allow_partial = false); virtual size_t Read(void *buffer, size_t count, bool allow_partial = false); virtual void Write(const void *buffer, size_t count); virtual void Close(void); diff --git a/lib/base/stream.cpp b/lib/base/stream.cpp index 5b465cba3..524cd0782 100644 --- a/lib/base/stream.cpp +++ b/lib/base/stream.cpp @@ -40,6 +40,16 @@ bool Stream::IsDataAvailable(void) const return false; } +void Stream::Shutdown(void) +{ + BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support Shutdown().")); +} + +size_t Stream::Peek(void *buffer, size_t count, bool allow_partial) +{ + BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support Peek().")); +} + void Stream::SignalDataAvailable(void) { OnDataAvailable(); @@ -50,15 +60,20 @@ void Stream::SignalDataAvailable(void) } } -void Stream::WaitForData(void) +bool Stream::WaitForData(int timeout) { if (!SupportsWaiting()) BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support waiting.")); boost::mutex::scoped_lock lock(m_Mutex); - while (!IsDataAvailable()) - m_CV.wait(lock); + while (!IsDataAvailable() && !IsEof()) + if (timeout < 0) + m_CV.wait(lock); + else + m_CV.timed_wait(lock, boost::posix_time::milliseconds(timeout * 1000)); + + return IsDataAvailable() || IsEof(); } StreamReadStatus Stream::ReadLine(String *line, StreamReadContext& context, bool may_wait) @@ -132,6 +147,7 @@ bool StreamReadContext::FillFromStream(const Stream::Ptr& stream, bool may_wait) void StreamReadContext::DropData(size_t count) { + ASSERT(count <= Size); memmove(Buffer, Buffer + count, Size - count); Size -= count; } diff --git a/lib/base/stream.hpp b/lib/base/stream.hpp index 9754d3b36..443504f1b 100644 --- a/lib/base/stream.hpp +++ b/lib/base/stream.hpp @@ -73,6 +73,17 @@ class I2_BASE_API Stream : public Object public: DECLARE_PTR_TYPEDEFS(Stream); + /** + * Reads data from the stream without removing it from the stream buffer. + * + * @param buffer The buffer where data should be stored. May be NULL if you're + * not actually interested in the data. + * @param count The number of bytes to read from the queue. + * @param allow_partial Whether to allow partial reads. + * @returns The number of bytes actually read. + */ + virtual size_t Peek(void *buffer, size_t count, bool allow_partial = false); + /** * Reads data from the stream. * @@ -93,6 +104,12 @@ public: */ virtual void Write(const void *buffer, size_t count) = 0; + /** + * Causes the stream to be closed (via Close()) once all pending data has been + * written. + */ + virtual void Shutdown(void); + /** * Closes the stream and releases resources. */ @@ -108,7 +125,7 @@ public: /** * Waits until data can be read from the stream. */ - void WaitForData(void); + bool WaitForData(int timeout = -1); virtual bool SupportsWaiting(void) const; diff --git a/lib/base/tlsstream.cpp b/lib/base/tlsstream.cpp index 6f6ab788b..2430ef29a 100644 --- a/lib/base/tlsstream.cpp +++ b/lib/base/tlsstream.cpp @@ -42,7 +42,7 @@ bool I2_EXPORT TlsStream::m_SSLIndexInitialized = false; TlsStream::TlsStream(const Socket::Ptr& socket, const String& hostname, ConnectionRole role, const boost::shared_ptr& sslContext) : SocketEvents(socket, this), m_Eof(false), m_HandshakeOK(false), m_VerifyOK(true), m_ErrorCode(0), m_ErrorOccurred(false), m_Socket(socket), m_Role(role), m_SendQ(new FIFO()), m_RecvQ(new FIFO()), - m_CurrentAction(TlsActionNone), m_Retry(false) + m_CurrentAction(TlsActionNone), m_Retry(false), m_Shutdown(false) { std::ostringstream msgbuf; char errbuf[120]; @@ -65,7 +65,7 @@ TlsStream::TlsStream(const Socket::Ptr& socket, const String& hostname, Connecti SSL_set_ex_data(m_SSL.get(), m_SSLIndex, this); - SSL_set_verify(m_SSL.get(), SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, &TlsStream::ValidateCertificate); + SSL_set_verify(m_SSL.get(), SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, &TlsStream::ValidateCertificate); socket->MakeNonBlocking(); @@ -160,7 +160,7 @@ void TlsStream::OnEvent(int revents) break; case TlsActionWrite: - count = m_SendQ->Peek(buffer, sizeof(buffer)); + count = m_SendQ->Peek(buffer, sizeof(buffer), true); rc = SSL_write(m_SSL.get(), buffer, count); @@ -191,9 +191,12 @@ void TlsStream::OnEvent(int revents) lock.unlock(); - if (m_RecvQ->IsDataAvailable()) + while (m_RecvQ->IsDataAvailable()) SignalDataAvailable(); + if (m_Shutdown && !m_SendQ->IsDataAvailable()) + Close(); + return; } @@ -232,6 +235,9 @@ void TlsStream::OnEvent(int revents) m_ErrorCode = ERR_peek_error(); m_ErrorOccurred = true; + Log(LogWarning, "TlsStream") + << "OpenSSL error: " << ERR_error_string(m_ErrorCode, NULL); + m_CV.notify_all(); break; @@ -263,6 +269,19 @@ void TlsStream::Handshake(void) /** * Processes data for the stream. */ +size_t TlsStream::Peek(void *buffer, size_t count, bool allow_partial) +{ + boost::mutex::scoped_lock lock(m_Mutex); + + if (!allow_partial) + while (m_RecvQ->GetAvailableBytes() < count && !m_ErrorOccurred && !m_Eof) + m_CV.wait(lock); + + HandleError(); + + return m_RecvQ->Peek(buffer, count, true); +} + size_t TlsStream::Read(void *buffer, size_t count, bool allow_partial) { boost::mutex::scoped_lock lock(m_Mutex); @@ -285,6 +304,11 @@ void TlsStream::Write(const void *buffer, size_t count) ChangeEvents(POLLIN|POLLOUT); } +void TlsStream::Shutdown(void) +{ + m_Shutdown = true; +} + /** * Closes the stream. */ diff --git a/lib/base/tlsstream.hpp b/lib/base/tlsstream.hpp index cc627b1b5..59f5d0a39 100644 --- a/lib/base/tlsstream.hpp +++ b/lib/base/tlsstream.hpp @@ -57,7 +57,9 @@ public: void Handshake(void); virtual void Close(void); + virtual void Shutdown(void); + virtual size_t Peek(void *buffer, size_t count, bool allow_partial = false); virtual size_t Read(void *buffer, size_t count, bool allow_partial = false); virtual void Write(const void *buffer, size_t count); @@ -86,6 +88,7 @@ private: TlsAction m_CurrentAction; bool m_Retry; + bool m_Shutdown; static int m_SSLIndex; static bool m_SSLIndexInitialized; diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index 5090d4d54..bba952a11 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -85,6 +85,7 @@ boost::shared_ptr MakeSSLContext(const String& pubkey, const String& pr boost::shared_ptr sslContext = boost::shared_ptr(SSL_CTX_new(TLSv1_method()), SSL_CTX_free); 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") diff --git a/lib/remote/CMakeLists.txt b/lib/remote/CMakeLists.txt index 2734d82e7..f4e29c0e5 100644 --- a/lib/remote/CMakeLists.txt +++ b/lib/remote/CMakeLists.txt @@ -21,8 +21,10 @@ mkclass_target(endpoint.ti endpoint.tcpp endpoint.thpp) mkclass_target(zone.ti zone.tcpp zone.thpp) set(remote_SOURCES - apiclient.cpp apiclient-heartbeat.cpp apifunction.cpp apilistener.cpp apilistener.thpp apilistener-sync.cpp - apiuser.cpp apiuser.thpp authority.cpp endpoint.cpp endpoint.thpp jsonrpc.cpp + apifunction.cpp apilistener.cpp apilistener.thpp apilistener-sync.cpp + apiuser.cpp apiuser.thpp authority.cpp endpoint.cpp endpoint.thpp + httpchunkedencoding.cpp httpconnection.cpp httpdemohandler.cpp httphandler.cpp httprequest.cpp httpresponse.cpp + jsonrpc.cpp jsonrpcconnection.cpp jsonrpcconnection-heartbeat.cpp messageorigin.cpp zone.cpp zone.thpp ) diff --git a/lib/remote/apilistener-sync.cpp b/lib/remote/apilistener-sync.cpp index 90fb086c3..21002109a 100644 --- a/lib/remote/apilistener-sync.cpp +++ b/lib/remote/apilistener-sync.cpp @@ -157,7 +157,7 @@ void ApiListener::SyncZoneDirs(void) const } } -void ApiListener::SendConfigUpdate(const ApiClient::Ptr& aclient) +void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient) { Endpoint::Ptr endpoint = aclient->GetEndpoint(); ASSERT(endpoint); diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index 7d218fe34..a982e1135 100644 --- a/lib/remote/apilistener.cpp +++ b/lib/remote/apilistener.cpp @@ -19,7 +19,7 @@ #include "remote/apilistener.hpp" #include "remote/apilistener.tcpp" -#include "remote/apiclient.hpp" +#include "remote/jsonrpcconnection.hpp" #include "remote/endpoint.hpp" #include "remote/jsonrpc.hpp" #include "base/convert.hpp" @@ -211,7 +211,8 @@ void ApiListener::ListenerThreadProc(const Socket::Ptr& server) for (;;) { try { Socket::Ptr client = server->Accept(); - Utility::QueueAsyncCallback(boost::bind(&ApiListener::NewClientHandler, this, client, String(), RoleServer), LowLatencyScheduler); + boost::thread thread(boost::bind(&ApiListener::NewClientHandler, this, client, String(), RoleServer)); + thread.detach(); } catch (const std::exception&) { Log(LogCritical, "ApiListener", "Cannot accept new connection."); } @@ -239,7 +240,7 @@ void ApiListener::AddConnection(const Endpoint::Ptr& endpoint) String host = endpoint->GetHost(); String port = endpoint->GetPort(); - Log(LogInformation, "ApiClient") + Log(LogInformation, "JsonRpcConnection") << "Reconnecting to API endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'"; TcpSocket::Ptr client = new TcpSocket(); @@ -284,56 +285,98 @@ void ApiListener::NewClientHandler(const Socket::Ptr& client, const String& host try { tlsStream->Handshake(); - } catch (const std::exception&) { - Log(LogCritical, "ApiListener", "Client TLS handshake failed."); + } catch (const std::exception& ex) { + Log(LogCritical, "ApiListener", "Client TLS handshake failed"); return; } boost::shared_ptr cert = tlsStream->GetPeerCertificate(); String identity; - - try { - identity = GetCertificateCN(cert); - } catch (const std::exception&) { - Log(LogCritical, "ApiListener") - << "Cannot get certificate common name from cert path: '" << GetCertPath() << "'."; - return; - } - - bool verify_ok = tlsStream->IsVerifyOK(); - - Log(LogInformation, "ApiListener") - << "New client connection for identity '" << identity << "'" << (verify_ok ? "" : " (unauthenticated)"); - Endpoint::Ptr endpoint; + bool verify_ok = false; - if (verify_ok) - endpoint = Endpoint::GetByName(identity); + if (cert) { + try { + identity = GetCertificateCN(cert); + } catch (const std::exception&) { + Log(LogCritical, "ApiListener") + << "Cannot get certificate common name from cert path: '" << GetCertPath() << "'."; + return; + } + + verify_ok = tlsStream->IsVerifyOK(); + + Log(LogInformation, "ApiListener") + << "New client connection for identity '" << identity << "'" << (verify_ok ? "" : " (unauthenticated)"); + + + if (verify_ok) + endpoint = Endpoint::GetByName(identity); + } else { + Log(LogInformation, "ApiListener") + << "New client connection (no client certificate)"; + } bool need_sync = false; if (endpoint) need_sync = !endpoint->IsConnected(); - ApiClient::Ptr aclient = new ApiClient(identity, verify_ok, tlsStream, role); - aclient->Start(); + ClientType ctype; - if (endpoint) { - endpoint->AddClient(aclient); + if (role == RoleClient) { + Dictionary::Ptr message = new Dictionary(); + message->Set("jsonrpc", "2.0"); + message->Set("method", "icinga::Hello"); + message->Set("params", new Dictionary()); + JsonRpc::SendMessage(tlsStream, message); + ctype = ClientJsonRpc; + } else { + tlsStream->WaitForData(5); - if (need_sync) { - { - ObjectLock olock(endpoint); - - endpoint->SetSyncing(true); - } - - ReplayLog(aclient); + if (!tlsStream->IsDataAvailable()) { + Log(LogWarning, "ApiListener", "No data received on new API connection."); + return; } - SendConfigUpdate(aclient); - } else - AddAnonymousClient(aclient); + char firstByte; + tlsStream->Peek(&firstByte, 1, false); + + if (firstByte >= '0' && firstByte <= '9') + ctype = ClientJsonRpc; + else + ctype = ClientHttp; + } + + if (ctype == ClientJsonRpc) { + Log(LogInformation, "ApiListener", "New JSON-RPC client"); + + JsonRpcConnection::Ptr aclient = new JsonRpcConnection(identity, verify_ok, tlsStream, role); + aclient->Start(); + + if (endpoint) { + endpoint->AddClient(aclient); + + if (need_sync) { + { + ObjectLock olock(endpoint); + + endpoint->SetSyncing(true); + } + + ReplayLog(aclient); + } + + SendConfigUpdate(aclient); + } else + AddAnonymousClient(aclient); + } else { + Log(LogInformation, "ApiListener", "New HTTP client"); + + HttpConnection::Ptr aclient = new HttpConnection(identity, verify_ok, tlsStream); + aclient->Start(); + AddHttpClient(aclient); + } } void ApiListener::ApiTimerHandler(void) @@ -429,7 +472,7 @@ void ApiListener::ApiTimerHandler(void) lmessage->Set("method", "log::SetLogPosition"); lmessage->Set("params", lparams); - BOOST_FOREACH(const ApiClient::Ptr& client, endpoint->GetClients()) + BOOST_FOREACH(const JsonRpcConnection::Ptr& client, endpoint->GetClients()) client->SendMessage(lmessage); Log(LogNotice, "ApiListener") @@ -495,7 +538,7 @@ void ApiListener::SyncSendMessage(const Endpoint::Ptr& endpoint, const Dictionar Log(LogNotice, "ApiListener") << "Sending message to '" << endpoint->GetName() << "'"; - BOOST_FOREACH(const ApiClient::Ptr& client, endpoint->GetClients()) + BOOST_FOREACH(const JsonRpcConnection::Ptr& client, endpoint->GetClients()) client->SendMessage(message); } } @@ -635,7 +678,7 @@ void ApiListener::LogGlobHandler(std::vector& files, const String& file) files.push_back(ts); } -void ApiListener::ReplayLog(const ApiClient::Ptr& client) +void ApiListener::ReplayLog(const JsonRpcConnection::Ptr& client) { Endpoint::Ptr endpoint = client->GetEndpoint(); @@ -823,20 +866,38 @@ std::pair ApiListener::GetStatus(void) return std::make_pair(status, perfdata); } -void ApiListener::AddAnonymousClient(const ApiClient::Ptr& aclient) +void ApiListener::AddAnonymousClient(const JsonRpcConnection::Ptr& aclient) { ObjectLock olock(this); m_AnonymousClients.insert(aclient); } -void ApiListener::RemoveAnonymousClient(const ApiClient::Ptr& aclient) +void ApiListener::RemoveAnonymousClient(const JsonRpcConnection::Ptr& aclient) { ObjectLock olock(this); m_AnonymousClients.erase(aclient); } -std::set ApiListener::GetAnonymousClients(void) const +std::set ApiListener::GetAnonymousClients(void) const { ObjectLock olock(this); return m_AnonymousClients; } + +void ApiListener::AddHttpClient(const HttpConnection::Ptr& aclient) +{ + ObjectLock olock(this); + m_HttpClients.insert(aclient); +} + +void ApiListener::RemoveHttpClient(const HttpConnection::Ptr& aclient) +{ + ObjectLock olock(this); + m_HttpClients.erase(aclient); +} + +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 93aba2ab9..c7fccb38b 100644 --- a/lib/remote/apilistener.hpp +++ b/lib/remote/apilistener.hpp @@ -21,7 +21,8 @@ #define APILISTENER_H #include "remote/apilistener.thpp" -#include "remote/apiclient.hpp" +#include "remote/jsonrpcconnection.hpp" +#include "remote/httpconnection.hpp" #include "remote/endpoint.hpp" #include "remote/messageorigin.hpp" #include "base/dynamicobject.hpp" @@ -34,7 +35,7 @@ namespace icinga { -class ApiClient; +class JsonRpcConnection; /** * @ingroup remote @@ -64,9 +65,13 @@ public: static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); std::pair GetStatus(void); - void AddAnonymousClient(const ApiClient::Ptr& aclient); - void RemoveAnonymousClient(const ApiClient::Ptr& aclient); - std::set GetAnonymousClients(void) const; + void AddAnonymousClient(const JsonRpcConnection::Ptr& aclient); + 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; static Value ConfigUpdateHandler(const MessageOrigin& origin, const Dictionary::Ptr& params); @@ -78,7 +83,8 @@ protected: private: boost::shared_ptr m_SSLContext; std::set m_Servers; - std::set m_AnonymousClients; + std::set m_AnonymousClients; + std::set m_HttpClients; Timer::Ptr m_Timer; void ApiTimerHandler(void); @@ -102,7 +108,7 @@ private: void RotateLogFile(void); void CloseLogFile(void); static void LogGlobHandler(std::vector& files, const String& file); - void ReplayLog(const ApiClient::Ptr& client); + void ReplayLog(const JsonRpcConnection::Ptr& client); static Dictionary::Ptr LoadConfigDir(const String& dir); static bool UpdateConfigDir(const Dictionary::Ptr& oldConfig, const Dictionary::Ptr& newConfig, const String& configDir, bool authoritative); @@ -112,7 +118,7 @@ private: static bool IsConfigMaster(const Zone::Ptr& zone); static void ConfigGlobHandler(Dictionary::Ptr& config, const String& path, const String& file); - void SendConfigUpdate(const ApiClient::Ptr& aclient); + void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient); }; } diff --git a/lib/remote/endpoint.cpp b/lib/remote/endpoint.cpp index 4dca3970b..71612c318 100644 --- a/lib/remote/endpoint.cpp +++ b/lib/remote/endpoint.cpp @@ -20,7 +20,7 @@ #include "remote/endpoint.hpp" #include "remote/endpoint.tcpp" #include "remote/apilistener.hpp" -#include "remote/apiclient.hpp" +#include "remote/jsonrpcconnection.hpp" #include "remote/zone.hpp" #include "base/dynamictype.hpp" #include "base/utility.hpp" @@ -32,8 +32,8 @@ using namespace icinga; REGISTER_TYPE(Endpoint); -boost::signals2::signal Endpoint::OnConnected; -boost::signals2::signal Endpoint::OnDisconnected; +boost::signals2::signal Endpoint::OnConnected; +boost::signals2::signal Endpoint::OnDisconnected; void Endpoint::OnAllConfigLoaded(void) { @@ -57,7 +57,7 @@ void Endpoint::OnAllConfigLoaded(void) BOOST_THROW_EXCEPTION(ScriptError("Endpoint '" + GetName() + "' does not belong to a zone.", GetDebugInfo())); } -void Endpoint::AddClient(const ApiClient::Ptr& client) +void Endpoint::AddClient(const JsonRpcConnection::Ptr& client) { bool was_master = ApiListener::GetInstance()->IsMaster(); @@ -74,7 +74,7 @@ void Endpoint::AddClient(const ApiClient::Ptr& client) OnConnected(this, client); } -void Endpoint::RemoveClient(const ApiClient::Ptr& client) +void Endpoint::RemoveClient(const JsonRpcConnection::Ptr& client) { bool was_master = ApiListener::GetInstance()->IsMaster(); @@ -96,7 +96,7 @@ void Endpoint::RemoveClient(const ApiClient::Ptr& client) OnDisconnected(this, client); } -std::set Endpoint::GetClients(void) const +std::set Endpoint::GetClients(void) const { boost::mutex::scoped_lock lock(m_ClientsLock); return m_Clients; diff --git a/lib/remote/endpoint.hpp b/lib/remote/endpoint.hpp index 3411b32aa..e02438e07 100644 --- a/lib/remote/endpoint.hpp +++ b/lib/remote/endpoint.hpp @@ -27,7 +27,7 @@ namespace icinga { -class ApiClient; +class JsonRpcConnection; class Zone; /** @@ -41,12 +41,12 @@ public: DECLARE_OBJECT(Endpoint); DECLARE_OBJECTNAME(Endpoint); - static boost::signals2::signal&)> OnConnected; - static boost::signals2::signal&)> OnDisconnected; + static boost::signals2::signal&)> OnConnected; + static boost::signals2::signal&)> OnDisconnected; - void AddClient(const intrusive_ptr& client); - void RemoveClient(const intrusive_ptr& client); - std::set > GetClients(void) const; + void AddClient(const intrusive_ptr& client); + void RemoveClient(const intrusive_ptr& client); + std::set > GetClients(void) const; intrusive_ptr GetZone(void) const; @@ -59,7 +59,7 @@ protected: private: mutable boost::mutex m_ClientsLock; - std::set > m_Clients; + std::set > m_Clients; intrusive_ptr m_Zone; }; diff --git a/lib/remote/httpchunkedencoding.cpp b/lib/remote/httpchunkedencoding.cpp new file mode 100644 index 000000000..81b815c70 --- /dev/null +++ b/lib/remote/httpchunkedencoding.cpp @@ -0,0 +1,79 @@ +/****************************************************************************** + * 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/httpchunkedencoding.hpp" +#include + +using namespace icinga; + +StreamReadStatus HttpChunkedEncoding::ReadChunkFromStream(const Stream::Ptr& stream, + char **data, size_t *size, ChunkReadContext& context, bool may_wait) +{ + if (context.LengthIndicator == -1) { + String line; + StreamReadStatus status = stream->ReadLine(&line, context.StreamContext, may_wait); + + if (status != StatusNewItem) + return status; + + std::stringstream msgbuf; + 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; + } + + if (scontext.Size < context.LengthIndicator) { + scontext.MustRead = true; + return StatusNeedData; + } + + *data = new char[context.LengthIndicator]; + *size = context.LengthIndicator; + memcpy(data, scontext.Buffer, context.LengthIndicator); + + scontext.DropData(context.LengthIndicator); + context.LengthIndicator = -1; + + return StatusNewItem; + } +} + +void HttpChunkedEncoding::WriteChunkToStream(const Stream::Ptr& stream, const char *data, size_t count) +{ + std::ostringstream msgbuf; + msgbuf << std::hex << count << "\r\n"; + String lengthIndicator = msgbuf.str(); + stream->Write(lengthIndicator.CStr(), lengthIndicator.GetLength()); + stream->Write(data, count); + if (count > 0) + stream->Write("\r\n", 2); +} diff --git a/lib/remote/httpchunkedencoding.hpp b/lib/remote/httpchunkedencoding.hpp new file mode 100644 index 000000000..70661c90b --- /dev/null +++ b/lib/remote/httpchunkedencoding.hpp @@ -0,0 +1,54 @@ +/****************************************************************************** + * 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 HTTPCHUNKEDENCODING_H +#define HTTPCHUNKEDENCODING_H + +#include "remote/i2-remote.hpp" +#include "base/stream.hpp" + +namespace icinga +{ + +struct ChunkReadContext +{ + StreamReadContext& StreamContext; + int LengthIndicator; + + ChunkReadContext(StreamReadContext& scontext) + : StreamContext(scontext), LengthIndicator(-1) + { } +}; + +/** + * HTTP chunked encoding. + * + * @ingroup remote + */ +struct I2_REMOTE_API HttpChunkedEncoding +{ + static StreamReadStatus ReadChunkFromStream(const Stream::Ptr& stream, + char **data, size_t *size, ChunkReadContext& ccontext, bool may_wait = false); + static void WriteChunkToStream(const Stream::Ptr& stream, const char *data, size_t count); + +}; + +} + +#endif /* HTTPCHUNKEDENCODING_H */ diff --git a/lib/remote/httpconnection.cpp b/lib/remote/httpconnection.cpp new file mode 100644 index 000000000..20fdee631 --- /dev/null +++ b/lib/remote/httpconnection.cpp @@ -0,0 +1,157 @@ +/****************************************************************************** + * 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/httpconnection.hpp" +#include "remote/httphandler.hpp" +#include "remote/apilistener.hpp" +#include "remote/apifunction.hpp" +#include "remote/jsonrpc.hpp" +#include "base/dynamictype.hpp" +#include "base/objectlock.hpp" +#include "base/utility.hpp" +#include "base/logger.hpp" +#include "base/exception.hpp" +#include "base/convert.hpp" +#include + +using namespace icinga; + +static boost::once_flag l_HttpConnectionOnceFlag = BOOST_ONCE_INIT; +static Timer::Ptr l_HttpConnectionTimeoutTimer; + +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) +{ + boost::call_once(l_HttpConnectionOnceFlag, &HttpConnection::StaticInitialize); + + if (authenticated) + m_ApiUser = ApiUser::GetByName(identity); +} + +void HttpConnection::StaticInitialize(void) +{ + l_HttpConnectionTimeoutTimer = new Timer(); + l_HttpConnectionTimeoutTimer->OnTimerExpired.connect(boost::bind(&HttpConnection::TimeoutTimerHandler)); + l_HttpConnectionTimeoutTimer->SetInterval(15); + l_HttpConnectionTimeoutTimer->Start(); +} + +void HttpConnection::Start(void) +{ + m_Stream->RegisterDataHandler(boost::bind(&HttpConnection::DataAvailableHandler, this)); + if (m_Stream->IsDataAvailable()) + DataAvailableHandler(); +} + +ApiUser::Ptr HttpConnection::GetApiUser(void) const +{ + return m_ApiUser; +} + +TlsStream::Ptr HttpConnection::GetStream(void) const +{ + return m_Stream; +} + +void HttpConnection::Disconnect(void) +{ + Log(LogDebug, "HttpConnection", "Http client disconnected"); + + ApiListener::Ptr listener = ApiListener::GetInstance(); + listener->RemoveHttpClient(this); + + m_Stream->Shutdown(); +} + +bool HttpConnection::ProcessMessage(void) +{ + bool res; + + try { + res = m_CurrentRequest.Parse(m_Stream, m_Context, false); + } catch (const std::exception& ex) { + HttpResponse response(m_Stream, m_CurrentRequest); + response.SetStatus(400, "Bad request"); + String msg = "

Bad request

"; + response.WriteBody(msg.CStr(), msg.GetLength()); + response.FinishBody(); + + m_Stream->Shutdown(); + return false; + } + + if (m_CurrentRequest.Complete) { + m_RequestQueue.Enqueue(boost::bind(&HttpConnection::ProcessMessageAsync, HttpConnection::Ptr(this), m_CurrentRequest)); + + m_Seen = Utility::GetTime(); + m_PendingRequests++; + + m_CurrentRequest.~HttpRequest(); + new (&m_CurrentRequest) HttpRequest(m_Context); + + return true; + } + + return res; +} + +void HttpConnection::ProcessMessageAsync(HttpRequest& request) +{ + Log(LogInformation, "HttpConnection", "Processing Http message"); + + HttpResponse response(m_Stream, request); + HttpHandler::ProcessRequest(request, response); + response.Finish(); + + m_PendingRequests--; +} + +void HttpConnection::DataAvailableHandler(void) +{ + boost::mutex::scoped_lock lock(m_DataHandlerMutex); + + try { + while (ProcessMessage()) + ; /* empty loop body */ + } catch (const std::exception& ex) { + Log(LogWarning, "HttpConnection") + << "Error while reading Http request: " << DiagnosticInformation(ex); + + Disconnect(); + } +} + +void HttpConnection::CheckLiveness(void) +{ + if (m_Seen < Utility::GetTime() - 10 && m_PendingRequests == 0) { + Log(LogInformation, "HttpConnection") + << "No messages for Http connection have been received in the last 10 seconds."; + Disconnect(); + } +} + +void HttpConnection::TimeoutTimerHandler(void) +{ + ApiListener::Ptr listener = ApiListener::GetInstance(); + + BOOST_FOREACH(const HttpConnection::Ptr& client, listener->GetHttpClients()) { + client->CheckLiveness(); + } +} diff --git a/lib/remote/httpconnection.hpp b/lib/remote/httpconnection.hpp new file mode 100644 index 000000000..a1ca8cf7d --- /dev/null +++ b/lib/remote/httpconnection.hpp @@ -0,0 +1,75 @@ +/****************************************************************************** + * 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 HTTPCONNECTION_H +#define HTTPCONNECTION_H + +#include "remote/httprequest.hpp" +#include "remote/apiuser.hpp" +#include "base/tlsstream.hpp" +#include "base/timer.hpp" +#include "base/workqueue.hpp" + +namespace icinga +{ + +/** + * An API client connection. + * + * @ingroup remote + */ +class I2_REMOTE_API HttpConnection : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(HttpConnection); + + HttpConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream); + + void Start(void); + + ApiUser::Ptr GetApiUser(void) const; + bool IsAuthenticated(void) const; + TlsStream::Ptr GetStream(void) const; + + void Disconnect(void); + +private: + ApiUser::Ptr m_ApiUser; + TlsStream::Ptr m_Stream; + double m_Seen; + HttpRequest m_CurrentRequest; + boost::mutex m_DataHandlerMutex; + WorkQueue m_RequestQueue; + int m_PendingRequests; + + StreamReadContext m_Context; + + bool ProcessMessage(void); + void DataAvailableHandler(void); + + static void StaticInitialize(void); + static void TimeoutTimerHandler(void); + void CheckLiveness(void); + + void ProcessMessageAsync(HttpRequest& request); +}; + +} + +#endif /* HTTPCONNECTION_H */ diff --git a/lib/remote/httpdemohandler.cpp b/lib/remote/httpdemohandler.cpp new file mode 100644 index 000000000..0553ba802 --- /dev/null +++ b/lib/remote/httpdemohandler.cpp @@ -0,0 +1,46 @@ +/****************************************************************************** + * 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/httpdemohandler.hpp" + +using namespace icinga; + +REGISTER_URLHANDLER("/demo", HttpDemoHandler); + +void HttpDemoHandler::HandleRequest(HttpRequest& request, HttpResponse& response) +{ + if (request.RequestMethod == "GET") { + String form = "
"; + response.SetStatus(200, "OK"); + response.AddHeader("Content-Type", "text/html"); + response.WriteBody(form.CStr(), form.GetLength()); + } else if (request.RequestMethod == "POST") { + response.SetStatus(200, "OK"); + String msg = "You sent: "; + + char buffer[512]; + size_t count; + while ((count = request.ReadBody(buffer, sizeof(buffer))) > 0) + msg += String(buffer, buffer + count); + response.WriteBody(msg.CStr(), msg.GetLength()); + } else { + response.SetStatus(400, "Bad request"); + } +} + diff --git a/lib/remote/httpdemohandler.hpp b/lib/remote/httpdemohandler.hpp new file mode 100644 index 000000000..1e596e421 --- /dev/null +++ b/lib/remote/httpdemohandler.hpp @@ -0,0 +1,38 @@ +/****************************************************************************** + * 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 HTTPDEMOHANDLER_H +#define HTTPDEMOHANDLER_H + +#include "remote/httphandler.hpp" + +namespace icinga +{ + +class I2_REMOTE_API HttpDemoHandler : public HttpHandler +{ +public: + DECLARE_PTR_TYPEDEFS(HttpDemoHandler); + + virtual void HandleRequest(HttpRequest& request, HttpResponse& response); +}; + +} + +#endif /* HTTPDEMOHANDLER_H */ diff --git a/lib/remote/httphandler.cpp b/lib/remote/httphandler.cpp new file mode 100644 index 000000000..8c0802297 --- /dev/null +++ b/lib/remote/httphandler.cpp @@ -0,0 +1,98 @@ +/****************************************************************************** + * 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/httphandler.hpp" +#include "base/singleton.hpp" + +using namespace icinga; + +Dictionary::Ptr HttpHandler::m_UrlTree; + +void HttpHandler::Register(const Url::Ptr& url, const HttpHandler::Ptr& handler) +{ + if (!m_UrlTree) + m_UrlTree = new Dictionary(); + + Dictionary::Ptr node = m_UrlTree; + + BOOST_FOREACH(const String& elem, url->GetPath()) { + Dictionary::Ptr children = node->Get("children"); + + if (!children) { + children = new Dictionary(); + node->Set("children", children); + } + + Dictionary::Ptr sub_node = new Dictionary(); + children->Set(elem, sub_node); + + node = sub_node; + } + + node->Set("handler", handler); +} + +bool HttpHandler::CanAlsoHandleUrl(const Url::Ptr& url) const +{ + return false; +} + +void HttpHandler::ProcessRequest(HttpRequest& request, HttpResponse& response) +{ + Dictionary::Ptr node = m_UrlTree; + HttpHandler::Ptr current_handler, handler; + bool exact_match = true; + + BOOST_FOREACH(const String& elem, request.Url->GetPath()) { + current_handler = node->Get("handler"); + if (current_handler) + handler = current_handler; + + Dictionary::Ptr children = node->Get("children"); + + if (!children) { + exact_match = false; + node.reset(); + break; + } + + node = children->Get(elem); + + if (!node) { + exact_match = false; + break; + } + } + + if (node) { + current_handler = node->Get("handler"); + if (current_handler) + handler = current_handler; + } + + if (!handler || (!exact_match && !handler->CanAlsoHandleUrl(request.Url))) { + response.SetStatus(404, "Not found"); + String msg = "

Not found

"; + response.WriteBody(msg.CStr(), msg.GetLength()); + response.FinishBody(); + return; + } + + handler->HandleRequest(request, response); +} diff --git a/lib/remote/httphandler.hpp b/lib/remote/httphandler.hpp new file mode 100644 index 000000000..6456af2b2 --- /dev/null +++ b/lib/remote/httphandler.hpp @@ -0,0 +1,76 @@ +/****************************************************************************** + * 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 HTTPHANDLER_H +#define HTTPHANDLER_H + +#include "remote/i2-remote.hpp" +#include "remote/httpresponse.hpp" +#include "base/registry.hpp" +#include +#include + +namespace icinga +{ + +/** + * HTTP handler. + * + * @ingroup remote + */ +class I2_REMOTE_API HttpHandler : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(HttpHandler); + + virtual bool CanAlsoHandleUrl(const Url::Ptr& url) const; + virtual void HandleRequest(HttpRequest& request, HttpResponse& response) = 0; + + static void Register(const Url::Ptr& url, const HttpHandler::Ptr& handler); + static void ProcessRequest(HttpRequest& request, HttpResponse& response); + +private: + static Dictionary::Ptr m_UrlTree; +}; + +/** + * Helper class for registering HTTP handlers. + * + * @ingroup remote + */ +class I2_REMOTE_API RegisterHttpHandler +{ +public: + RegisterHttpHandler(const String& url, const HttpHandler& function); +}; + +#define REGISTER_URLHANDLER(url, klass) \ + namespace { namespace UNIQUE_NAME(apif) { namespace apif ## name { \ + void RegisterHandler(void) \ + { \ + Url::Ptr uurl = new Url(url); \ + HttpHandler::Ptr handler = new klass(); \ + HttpHandler::Register(uurl, handler); \ + } \ + INITIALIZE_ONCE(RegisterHandler); \ + } } } + +} + +#endif /* HTTPHANDLER_H */ diff --git a/lib/remote/httprequest.cpp b/lib/remote/httprequest.cpp new file mode 100644 index 000000000..57f5bcadf --- /dev/null +++ b/lib/remote/httprequest.cpp @@ -0,0 +1,154 @@ +/****************************************************************************** + * 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/httprequest.hpp" +#include "base/logger.hpp" +#include "base/convert.hpp" +#include +#include +#include + +using namespace icinga; + +HttpRequest::HttpRequest(StreamReadContext& src) + : m_State(HttpRequestStart), m_Context(src), + m_ChunkContext(m_Context), + ProtocolVersion(HttpVersion10), + Complete(false), + Headers(new Dictionary()) +{ } + +bool HttpRequest::Parse(const Stream::Ptr& stream, StreamReadContext& src, bool may_wait) +{ + if (m_State != HttpRequestBody) { + String line; + StreamReadStatus srs = stream->ReadLine(&line, src, may_wait); + + if (srs != StatusNewItem) + return false; + + if (m_State == HttpRequestStart) { + /* ignore trailing new-lines */ + if (line == "") + return true; + + std::vector tokens; + boost::algorithm::split(tokens, line, boost::is_any_of(" ")); + Log(LogWarning, "HttpRequest") + << "line: " << line << ", tokens: " << tokens.size(); + if (tokens.size() != 3) + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid HTTP request")); + RequestMethod = tokens[0]; + Url = new class Url(tokens[1]); + + if (tokens[2] == "HTTP/1.0") + ProtocolVersion = HttpVersion10; + else if (tokens[2] == "HTTP/1.1") { + ProtocolVersion = HttpVersion11; + } else + BOOST_THROW_EXCEPTION(std::invalid_argument("Unsupported HTTP version")); + + m_State = HttpRequestHeaders; + Log(LogWarning, "HttpRequest") + << "Method: " << RequestMethod << ", Url: " << Url; + } else if (m_State == HttpRequestHeaders) { + if (line == "") { + m_State = HttpRequestBody; + + /* 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(); + + Log(LogWarning, "HttpRequest", "Waiting for message body"); + 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); + boost::algorithm::to_lower(key); + key.Trim(); + String value = line.SubStr(pos + 1); + value.Trim(); + Headers->Set(key, value); + } + } else { + VERIFY(!"Invalid HTTP request state."); + } + } else if (m_State == HttpRequestBody) { + if (Headers->Get("transfer-encoding") == "chunked") { + char *data; + size_t size; + StreamReadStatus srs = HttpChunkedEncoding::ReadChunkFromStream(stream, &data, &size, m_ChunkContext, false); + + if (srs != StatusNewItem) + return false; + + Log(LogInformation, "HttpRequest") + << "Read " << size << " bytes"; + + m_Body->Write(data, size); + + delete [] data; + + if (size == 0) { + Complete = true; + return true; + } + } else { + if (m_Context.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; + BOOST_THROW_EXCEPTION(std::invalid_argument("Unexpected EOF in HTTP body")); + } + + m_Context.MustRead = false; + } + + size_t length_indicator = Convert::ToLong(Headers->Get("content-length")); + + if (m_Context.Size < length_indicator) { + m_Context.MustRead = true; + return false; + } + + m_Body->Write(m_Context.Buffer, length_indicator); + m_Context.DropData(length_indicator); + Complete = true; + return true; + } + } + + return true; +} + +size_t HttpRequest::ReadBody(char *data, size_t count) +{ + if (!m_Body) + return 0; + else + return m_Body->Read(data, count, true); +} + diff --git a/lib/remote/httprequest.hpp b/lib/remote/httprequest.hpp new file mode 100644 index 000000000..c025012ea --- /dev/null +++ b/lib/remote/httprequest.hpp @@ -0,0 +1,77 @@ +/****************************************************************************** + * 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 HTTPREQUEST_H +#define HTTPREQUEST_H + +#include "remote/i2-remote.hpp" +#include "remote/httpchunkedencoding.hpp" +#include "base/stream.hpp" +#include "base/fifo.hpp" +#include "base/dictionary.hpp" +#include "base/url.hpp" + +namespace icinga +{ + +enum HttpVersion +{ + HttpVersion10, + HttpVersion11 +}; + +enum HttpRequestState +{ + HttpRequestStart, + HttpRequestHeaders, + HttpRequestBody +}; + +/** + * An HTTP request. + * + * @ingroup remote + */ +struct I2_REMOTE_API HttpRequest +{ +public: + bool Complete; + + String RequestMethod; + Url::Ptr Url; + HttpVersion ProtocolVersion; + + Dictionary::Ptr Headers; + + HttpRequest(StreamReadContext& ctx); + + bool Parse(const Stream::Ptr& stream, StreamReadContext& src, bool may_wait); + + size_t ReadBody(char *data, size_t count); + +private: + StreamReadContext& m_Context; + ChunkReadContext m_ChunkContext; + HttpRequestState m_State; + FIFO::Ptr m_Body; +}; + +} + +#endif /* HTTPREQUEST_H */ diff --git a/lib/remote/httpresponse.cpp b/lib/remote/httpresponse.cpp new file mode 100644 index 000000000..a1e9915c3 --- /dev/null +++ b/lib/remote/httpresponse.cpp @@ -0,0 +1,107 @@ +/****************************************************************************** + * 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/httpresponse.hpp" +#include "remote/httpchunkedencoding.hpp" +#include "base/logger.hpp" +#include "base/application.hpp" +#include "base/convert.hpp" + +using namespace icinga; + +HttpResponse::HttpResponse(const Stream::Ptr& stream, const HttpRequest& request) + : m_Stream(stream), m_Request(request), m_State(HttpResponseStart) +{ } + +void HttpResponse::SetStatus(int code, const String& message) +{ + ASSERT(m_State == HttpResponseStart); + ASSERT(code >= 100 && code <= 599); + ASSERT(!message.IsEmpty()); + + String status = "HTTP/"; + + if (m_Request.ProtocolVersion == HttpVersion10) + status += "1.0"; + else + status += "1.1"; + + status += " " + Convert::ToString(code) + " " + message + "\r\n"; + + m_Stream->Write(status.CStr(), status.GetLength()); + + m_State = HttpResponseHeaders; +} + +void HttpResponse::AddHeader(const String& key, const String& value) +{ + ASSERT(m_State = HttpResponseHeaders); + String header = key + ": " + value + "\r\n"; + m_Stream->Write(header.CStr(), header.GetLength()); +} + +void HttpResponse::FinishHeaders(void) +{ + if (m_State == HttpResponseHeaders) { + if (m_Request.ProtocolVersion == HttpVersion11) + AddHeader("Transfer-Encoding", "chunked"); + + AddHeader("Server", "Icinga/" + Application::GetVersion()); + m_Stream->Write("\r\n", 2); + m_State = HttpResponseBody; + } +} + +void HttpResponse::WriteBody(const char *data, size_t count) +{ + ASSERT(m_State == HttpResponseHeaders || m_State == HttpResponseBody); + + if (m_Request.ProtocolVersion == HttpVersion10) { + if (!m_Body) + m_Body = new FIFO(); + + m_Body->Write(data, count); + } else { + FinishHeaders(); + + HttpChunkedEncoding::WriteChunkToStream(m_Stream, data, count); + } +} + +void HttpResponse::Finish(void) +{ + if (m_Request.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 { + WriteBody(NULL, 0); + m_Stream->Write("\r\n", 2); + } + + if (m_Request.ProtocolVersion == HttpVersion10 || m_Request.Headers->Get("connection") == "close") + m_Stream->Shutdown(); +} diff --git a/lib/remote/httpresponse.hpp b/lib/remote/httpresponse.hpp new file mode 100644 index 000000000..1bb8b13c1 --- /dev/null +++ b/lib/remote/httpresponse.hpp @@ -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. * + ******************************************************************************/ + +#ifndef HTTPRESPONSE_H +#define HTTPRESPONSE_H + +#include "remote/httprequest.hpp" +#include "base/stream.hpp" +#include "base/fifo.hpp" + +namespace icinga +{ + +enum HttpResponseState +{ + HttpResponseStart, + HttpResponseHeaders, + HttpResponseBody +}; + +/** + * An HTTP response. + * + * @ingroup remote + */ +struct I2_REMOTE_API HttpResponse +{ +public: + HttpResponse(const Stream::Ptr& stream, const HttpRequest& request); + + void SetStatus(int code, const String& message); + void AddHeader(const String& key, const String& value); + void WriteBody(const char *data, size_t count); + void Finish(void); + +private: + HttpResponseState m_State; + const HttpRequest& m_Request; + Stream::Ptr m_Stream; + FIFO::Ptr m_Body; + + void FinishHeaders(void); +}; + +} + +#endif /* HTTPRESPONSE_H */ diff --git a/lib/remote/jsonrpc.cpp b/lib/remote/jsonrpc.cpp index b2a9c9bec..9447470d9 100644 --- a/lib/remote/jsonrpc.cpp +++ b/lib/remote/jsonrpc.cpp @@ -36,10 +36,10 @@ void JsonRpc::SendMessage(const Stream::Ptr& stream, const Dictionary::Ptr& mess NetString::WriteStringToStream(stream, json); } -StreamReadStatus JsonRpc::ReadMessage(const Stream::Ptr& stream, Dictionary::Ptr *message, StreamReadContext& src) +StreamReadStatus JsonRpc::ReadMessage(const Stream::Ptr& stream, Dictionary::Ptr *message, StreamReadContext& src, bool may_wait) { String jsonString; - StreamReadStatus srs = NetString::ReadStringFromStream(stream, &jsonString, src); + StreamReadStatus srs = NetString::ReadStringFromStream(stream, &jsonString, src, may_wait); if (srs != StatusNewItem) return srs; diff --git a/lib/remote/jsonrpc.hpp b/lib/remote/jsonrpc.hpp index 66bc032fe..0646db67d 100644 --- a/lib/remote/jsonrpc.hpp +++ b/lib/remote/jsonrpc.hpp @@ -36,7 +36,7 @@ class I2_REMOTE_API JsonRpc { public: static void SendMessage(const Stream::Ptr& stream, const Dictionary::Ptr& message); - static StreamReadStatus ReadMessage(const Stream::Ptr& stream, Dictionary::Ptr *message, StreamReadContext& src); + static StreamReadStatus ReadMessage(const Stream::Ptr& stream, Dictionary::Ptr *message, StreamReadContext& src, bool may_wait = false); private: JsonRpc(void); diff --git a/lib/remote/apiclient-heartbeat.cpp b/lib/remote/jsonrpcconnection-heartbeat.cpp similarity index 84% rename from lib/remote/apiclient-heartbeat.cpp rename to lib/remote/jsonrpcconnection-heartbeat.cpp index 4bd224e1c..2ed00b510 100644 --- a/lib/remote/apiclient-heartbeat.cpp +++ b/lib/remote/jsonrpcconnection-heartbeat.cpp @@ -17,7 +17,7 @@ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * ******************************************************************************/ -#include "remote/apiclient.hpp" +#include "remote/jsonrpcconnection.hpp" #include "remote/messageorigin.hpp" #include "remote/apifunction.hpp" #include "base/initialize.hpp" @@ -28,32 +28,32 @@ using namespace icinga; -REGISTER_APIFUNCTION(Heartbeat, event, &ApiClient::HeartbeatAPIHandler); +REGISTER_APIFUNCTION(Heartbeat, event, &JsonRpcConnection::HeartbeatAPIHandler); static Timer::Ptr l_HeartbeatTimer; static void StartHeartbeatTimer(void) { l_HeartbeatTimer = new Timer(); - l_HeartbeatTimer->OnTimerExpired.connect(boost::bind(&ApiClient::HeartbeatTimerHandler)); + l_HeartbeatTimer->OnTimerExpired.connect(boost::bind(&JsonRpcConnection::HeartbeatTimerHandler)); l_HeartbeatTimer->SetInterval(10); l_HeartbeatTimer->Start(); } INITIALIZE_ONCE(StartHeartbeatTimer); -void ApiClient::HeartbeatTimerHandler(void) +void JsonRpcConnection::HeartbeatTimerHandler(void) { BOOST_FOREACH(const Endpoint::Ptr& endpoint, DynamicType::GetObjectsByType()) { - BOOST_FOREACH(const ApiClient::Ptr& client, endpoint->GetClients()) { + BOOST_FOREACH(const JsonRpcConnection::Ptr& client, endpoint->GetClients()) { if (endpoint->GetSyncing()) { - Log(LogInformation, "ApiClient") + Log(LogInformation, "JsonRpcConnection") << "Not sending heartbeat for endpoint '" << endpoint->GetName() << "' because we're replaying the log for it."; continue; } if (client->m_NextHeartbeat != 0 && client->m_NextHeartbeat < Utility::GetTime()) { - Log(LogWarning, "ApiClient") + Log(LogWarning, "JsonRpcConnection") << "Client for endpoint '" << endpoint->GetName() << "' has requested " << "heartbeat message but hasn't responded in time. Closing connection."; @@ -75,7 +75,7 @@ void ApiClient::HeartbeatTimerHandler(void) } } -Value ApiClient::HeartbeatAPIHandler(const MessageOrigin& origin, const Dictionary::Ptr& params) +Value JsonRpcConnection::HeartbeatAPIHandler(const MessageOrigin& origin, const Dictionary::Ptr& params) { Value vtimeout = params->Get("timeout"); diff --git a/lib/remote/apiclient.cpp b/lib/remote/jsonrpcconnection.cpp similarity index 77% rename from lib/remote/apiclient.cpp rename to lib/remote/jsonrpcconnection.cpp index d4fc9f92a..063416830 100644 --- a/lib/remote/apiclient.cpp +++ b/lib/remote/jsonrpcconnection.cpp @@ -17,7 +17,7 @@ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * ******************************************************************************/ -#include "remote/apiclient.hpp" +#include "remote/jsonrpcconnection.hpp" #include "remote/apilistener.hpp" #include "remote/apifunction.hpp" #include "remote/jsonrpc.hpp" @@ -35,65 +35,65 @@ REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler); static Value RequestCertificateHandler(const MessageOrigin& origin, const Dictionary::Ptr& params); REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler); -static boost::once_flag l_ApiClientOnceFlag = BOOST_ONCE_INIT; -static Timer::Ptr l_ApiClientTimeoutTimer; +static boost::once_flag l_JsonRpcConnectionOnceFlag = BOOST_ONCE_INIT; +static Timer::Ptr l_JsonRpcConnectionTimeoutTimer; -ApiClient::ApiClient(const String& identity, bool authenticated, const TlsStream::Ptr& stream, ConnectionRole role) +JsonRpcConnection::JsonRpcConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream, ConnectionRole role) : m_Identity(identity), m_Authenticated(authenticated), m_Stream(stream), m_Role(role), m_Seen(Utility::GetTime()), m_NextHeartbeat(0), m_HeartbeatTimeout(0) { - boost::call_once(l_ApiClientOnceFlag, &ApiClient::StaticInitialize); + boost::call_once(l_JsonRpcConnectionOnceFlag, &JsonRpcConnection::StaticInitialize); if (authenticated) m_Endpoint = Endpoint::GetByName(identity); } -void ApiClient::StaticInitialize(void) +void JsonRpcConnection::StaticInitialize(void) { - l_ApiClientTimeoutTimer = new Timer(); - l_ApiClientTimeoutTimer->OnTimerExpired.connect(boost::bind(&ApiClient::TimeoutTimerHandler)); - l_ApiClientTimeoutTimer->SetInterval(15); - l_ApiClientTimeoutTimer->Start(); + l_JsonRpcConnectionTimeoutTimer = new Timer(); + l_JsonRpcConnectionTimeoutTimer->OnTimerExpired.connect(boost::bind(&JsonRpcConnection::TimeoutTimerHandler)); + l_JsonRpcConnectionTimeoutTimer->SetInterval(15); + l_JsonRpcConnectionTimeoutTimer->Start(); } -void ApiClient::Start(void) +void JsonRpcConnection::Start(void) { - m_Stream->RegisterDataHandler(boost::bind(&ApiClient::DataAvailableHandler, this)); + m_Stream->RegisterDataHandler(boost::bind(&JsonRpcConnection::DataAvailableHandler, this)); if (m_Stream->IsDataAvailable()) DataAvailableHandler(); } -String ApiClient::GetIdentity(void) const +String JsonRpcConnection::GetIdentity(void) const { return m_Identity; } -bool ApiClient::IsAuthenticated(void) const +bool JsonRpcConnection::IsAuthenticated(void) const { return m_Authenticated; } -Endpoint::Ptr ApiClient::GetEndpoint(void) const +Endpoint::Ptr JsonRpcConnection::GetEndpoint(void) const { return m_Endpoint; } -TlsStream::Ptr ApiClient::GetStream(void) const +TlsStream::Ptr JsonRpcConnection::GetStream(void) const { return m_Stream; } -ConnectionRole ApiClient::GetRole(void) const +ConnectionRole JsonRpcConnection::GetRole(void) const { return m_Role; } -void ApiClient::SendMessage(const Dictionary::Ptr& message) +void JsonRpcConnection::SendMessage(const Dictionary::Ptr& message) { - m_WriteQueue.Enqueue(boost::bind(&ApiClient::SendMessageSync, ApiClient::Ptr(this), message)); + m_WriteQueue.Enqueue(boost::bind(&JsonRpcConnection::SendMessageSync, JsonRpcConnection::Ptr(this), message)); } -void ApiClient::SendMessageSync(const Dictionary::Ptr& message) +void JsonRpcConnection::SendMessageSync(const Dictionary::Ptr& message) { try { ObjectLock olock(m_Stream); @@ -103,18 +103,18 @@ void ApiClient::SendMessageSync(const Dictionary::Ptr& message) } catch (const std::exception& ex) { std::ostringstream info; info << "Error while sending JSON-RPC message for identity '" << m_Identity << "'"; - Log(LogWarning, "ApiClient") + Log(LogWarning, "JsonRpcConnection") << info.str(); - Log(LogDebug, "ApiClient") + Log(LogDebug, "JsonRpcConnection") << info.str() << "\n" << DiagnosticInformation(ex); Disconnect(); } } -void ApiClient::Disconnect(void) +void JsonRpcConnection::Disconnect(void) { - Log(LogWarning, "ApiClient") + Log(LogWarning, "JsonRpcConnection") << "API client disconnected for identity '" << m_Identity << "'"; if (m_Endpoint) @@ -127,11 +127,11 @@ void ApiClient::Disconnect(void) m_Stream->Close(); } -bool ApiClient::ProcessMessage(void) +bool JsonRpcConnection::ProcessMessage(void) { Dictionary::Ptr message; - StreamReadStatus srs = JsonRpc::ReadMessage(m_Stream, &message, m_Context); + StreamReadStatus srs = JsonRpc::ReadMessage(m_Stream, &message, m_Context, false); if (srs != StatusNewItem) return false; @@ -163,7 +163,7 @@ bool ApiClient::ProcessMessage(void) String method = message->Get("method"); - Log(LogNotice, "ApiClient") + Log(LogNotice, "JsonRpcConnection") << "Received '" << method << "' message from '" << m_Identity << "'"; Dictionary::Ptr resultMessage = new Dictionary(); @@ -180,9 +180,9 @@ bool ApiClient::ProcessMessage(void) resultMessage->Set("error", DiagnosticInformation(ex)); std::ostringstream info; info << "Error while processing message for identity '" << m_Identity << "'"; - Log(LogWarning, "ApiClient") + Log(LogWarning, "JsonRpcConnection") << info.str(); - Log(LogDebug, "ApiClient") + Log(LogDebug, "JsonRpcConnection") << info.str() << "\n" << DiagnosticInformation(ex); } @@ -195,7 +195,7 @@ bool ApiClient::ProcessMessage(void) return true; } -void ApiClient::DataAvailableHandler(void) +void JsonRpcConnection::DataAvailableHandler(void) { boost::mutex::scoped_lock lock(m_DataHandlerMutex); @@ -203,7 +203,7 @@ void ApiClient::DataAvailableHandler(void) while (ProcessMessage()) ; /* empty loop body */ } catch (const std::exception& ex) { - Log(LogWarning, "ApiClient") + Log(LogWarning, "JsonRpcConnection") << "Error while reading JSON-RPC message for identity '" << m_Identity << "': " << DiagnosticInformation(ex); Disconnect(); @@ -267,25 +267,25 @@ Value RequestCertificateHandler(const MessageOrigin& origin, const Dictionary::P return result; } -void ApiClient::CheckLiveness(void) +void JsonRpcConnection::CheckLiveness(void) { if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) { - Log(LogInformation, "ApiClient") + Log(LogInformation, "JsonRpcConnection") << "No messages for identity '" << m_Identity << "' have been received in the last 60 seconds."; Disconnect(); } } -void ApiClient::TimeoutTimerHandler(void) +void JsonRpcConnection::TimeoutTimerHandler(void) { ApiListener::Ptr listener = ApiListener::GetInstance(); - BOOST_FOREACH(const ApiClient::Ptr& client, listener->GetAnonymousClients()) { + BOOST_FOREACH(const JsonRpcConnection::Ptr& client, listener->GetAnonymousClients()) { client->CheckLiveness(); } BOOST_FOREACH(const Endpoint::Ptr& endpoint, DynamicType::GetObjectsByType()) { - BOOST_FOREACH(const ApiClient::Ptr& client, endpoint->GetClients()) { + BOOST_FOREACH(const JsonRpcConnection::Ptr& client, endpoint->GetClients()) { client->CheckLiveness(); } } diff --git a/lib/remote/apiclient.hpp b/lib/remote/jsonrpcconnection.hpp similarity index 88% rename from lib/remote/apiclient.hpp rename to lib/remote/jsonrpcconnection.hpp index 3adf6161a..922eb6116 100644 --- a/lib/remote/apiclient.hpp +++ b/lib/remote/jsonrpcconnection.hpp @@ -17,8 +17,8 @@ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * ******************************************************************************/ -#ifndef APICLIENT_H -#define APICLIENT_H +#ifndef JSONRPCCONNECTION_H +#define JSONRPCCONNECTION_H #include "remote/endpoint.hpp" #include "base/tlsstream.hpp" @@ -35,6 +35,12 @@ enum ClientRole ClientOutbound }; +enum ClientType +{ + ClientJsonRpc, + ClientHttp +}; + struct MessageOrigin; /** @@ -42,12 +48,12 @@ struct MessageOrigin; * * @ingroup remote */ -class I2_REMOTE_API ApiClient : public Object +class I2_REMOTE_API JsonRpcConnection : public Object { public: - DECLARE_PTR_TYPEDEFS(ApiClient); + DECLARE_PTR_TYPEDEFS(JsonRpcConnection); - ApiClient(const String& identity, bool authenticated, const TlsStream::Ptr& stream, ConnectionRole role); + JsonRpcConnection(const String& identity, bool authenticated, const TlsStream::Ptr& stream, ConnectionRole role); void Start(void); @@ -73,7 +79,6 @@ private: double m_Seen; double m_NextHeartbeat; double m_HeartbeatTimeout; - Timer::Ptr m_TimeoutTimer; boost::mutex m_DataHandlerMutex; StreamReadContext m_Context; @@ -91,4 +96,4 @@ private: } -#endif /* APICLIENT_H */ +#endif /* JSONRPCCONNECTION_H */ diff --git a/lib/remote/messageorigin.hpp b/lib/remote/messageorigin.hpp index 705cddbca..8e52673d2 100644 --- a/lib/remote/messageorigin.hpp +++ b/lib/remote/messageorigin.hpp @@ -21,7 +21,7 @@ #define MESSAGEORIGIN_H #include "remote/zone.hpp" -#include "remote/apiclient.hpp" +#include "remote/jsonrpcconnection.hpp" namespace icinga { @@ -32,7 +32,7 @@ namespace icinga struct I2_REMOTE_API MessageOrigin { Zone::Ptr FromZone; - ApiClient::Ptr FromClient; + JsonRpcConnection::Ptr FromClient; bool IsLocal(void) const; }; diff --git a/lib/remote/zone.cpp b/lib/remote/zone.cpp index a5a21a63a..3d05cc430 100644 --- a/lib/remote/zone.cpp +++ b/lib/remote/zone.cpp @@ -19,7 +19,7 @@ #include "remote/zone.hpp" #include "remote/zone.tcpp" -#include "remote/apiclient.hpp" +#include "remote/jsonrpcconnection.hpp" #include "base/objectlock.hpp" #include