diff --git a/doc/12-icinga2-api.md b/doc/12-icinga2-api.md index 58aaf6873..0f44bbfda 100644 --- a/doc/12-icinga2-api.md +++ b/doc/12-icinga2-api.md @@ -208,30 +208,32 @@ The [regex function](18-library-reference.md#global-functions-regex) is availabl More information about filters can be found in the [filters](12-icinga2-api.md#icinga2-api-filters) chapter. +Permissions are tied to a maximum HTTP request size to prevent abuse, responses sent by Icinga are not limited. +An API user with all permissions ("\*") may send up to 512 MB regardless of the endpoint. + Available permissions for specific URL endpoints: - Permissions | URL Endpoint | Supports Filters - ------------------------------|---------------|----------------- - actions/<action> | /v1/actions | Yes - config/query | /v1/config | No - config/modify | /v1/config | No - console | /v1/console | No - events/<type> | /v1/events | No - objects/query/<type> | /v1/objects | Yes - objects/create/<type> | /v1/objects | No - objects/modify/<type> | /v1/objects | Yes - objects/delete/<type> | /v1/objects | Yes - status/query | /v1/status | Yes - templates/<type> | /v1/templates | Yes - types | /v1/types | Yes - variables | /v1/variables | Yes + Permissions | URL Endpoint | Supports filters | Max body size in MB + ------------------------------|---------------|-------------------|--------------------- + actions/<action> | /v1/actions | Yes | 1 + config/query | /v1/config | No | 1 + config/modify | /v1/config | No | 512 + console | /v1/console | No | 512 + events/<type> | /v1/events | No | 1 + objects/query/<type> | /v1/objects | Yes | 1 + objects/create/<type> | /v1/objects | No | 512 + objects/modify/<type> | /v1/objects | Yes | 512 + objects/delete/<type> | /v1/objects | Yes | 512 + status/query | /v1/status | Yes | 1 + templates/<type> | /v1/templates | Yes | 1 + types | /v1/types | Yes | 1 + variables | /v1/variables | Yes | 1 The required actions or types can be replaced by using a wildcard match ("\*"). ### Parameters -Depending on the request method there are two ways of -passing parameters to the request: +Depending on the request method there are two ways of passing parameters to the request: * JSON object as request body (all request methods other than `GET`) * Query string as URL parameter (all request methods) diff --git a/lib/base/stream.cpp b/lib/base/stream.cpp index 1e6b9baf1..72ca82c1a 100644 --- a/lib/base/stream.cpp +++ b/lib/base/stream.cpp @@ -60,7 +60,7 @@ void Stream::SignalDataAvailable() } } -bool Stream::WaitForData(int timeout) +bool Stream::WaitForData() { if (!SupportsWaiting()) BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support waiting.")); @@ -68,10 +68,25 @@ bool Stream::WaitForData(int timeout) boost::mutex::scoped_lock lock(m_Mutex); while (!IsDataAvailable() && !IsEof()) - if (timeout < 0) - m_CV.wait(lock); - else - m_CV.timed_wait(lock, boost::posix_time::milliseconds(timeout * 1000)); + m_CV.wait(lock); + + return IsDataAvailable() || IsEof(); +} + +bool Stream::WaitForData(int timeout) +{ + if (!SupportsWaiting()) + BOOST_THROW_EXCEPTION(std::runtime_error("Stream does not support waiting.")); + + if (timeout < 0) + BOOST_THROW_EXCEPTION(std::runtime_error("Timeout can't be negative")); + + boost::system_time const point_of_timeout = boost::get_system_time() + boost::posix_time::seconds(timeout); + + boost::mutex::scoped_lock lock(m_Mutex); + + while (!IsDataAvailable() && !IsEof() && point_of_timeout > boost::get_system_time()) + m_CV.timed_wait(lock, point_of_timeout); return IsDataAvailable() || IsEof(); } diff --git a/lib/base/stream.hpp b/lib/base/stream.hpp index 58569246c..8a140a586 100644 --- a/lib/base/stream.hpp +++ b/lib/base/stream.hpp @@ -122,8 +122,10 @@ public: /** * Waits until data can be read from the stream. + * Optionally with a timeout. */ - bool WaitForData(int timeout = -1); + bool WaitForData(); + bool WaitForData(int timeout); virtual bool SupportsWaiting() const; diff --git a/lib/base/tlsstream.cpp b/lib/base/tlsstream.cpp index 029c9b446..2846e8962 100644 --- a/lib/base/tlsstream.cpp +++ b/lib/base/tlsstream.cpp @@ -27,6 +27,8 @@ # include #endif /* _WIN32 */ +#define TLS_TIMEOUT_SECONDS 10 + using namespace icinga; int TlsStream::m_SSLIndex; @@ -285,8 +287,14 @@ void TlsStream::Handshake() m_CurrentAction = TlsActionHandshake; ChangeEvents(POLLOUT); - while (!m_HandshakeOK && !m_ErrorOccurred && !m_Eof) - m_CV.wait(lock); + boost::system_time const timeout = boost::get_system_time() + boost::posix_time::seconds(TLS_TIMEOUT_SECONDS); + + while (!m_HandshakeOK && !m_ErrorOccurred && !m_Eof && timeout > boost::get_system_time()) + m_CV.timed_wait(lock, timeout); + + // We should _NOT_ (underline, bold, itallic and wordart) throw an exception for a timeout. + if (timeout < boost::get_system_time()) + BOOST_THROW_EXCEPTION(std::runtime_error("Timeout during handshake.")); if (m_Eof) BOOST_THROW_EXCEPTION(std::runtime_error("Socket was closed during TLS handshake.")); diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp index 251aff791..bc0df4449 100644 --- a/lib/remote/apilistener.cpp +++ b/lib/remote/apilistener.cpp @@ -512,11 +512,17 @@ void ApiListener::NewClientHandlerInternal(const Socket::Ptr& client, const Stri JsonRpc::SendMessage(tlsStream, message); ctype = ClientJsonRpc; } else { - tlsStream->WaitForData(5); + tlsStream->WaitForData(10); if (!tlsStream->IsDataAvailable()) { - Log(LogWarning, "ApiListener") - << "No data received on new API connection for identity '" << identity << "'. Ensure that the remote endpoints are properly configured in a cluster setup."; + if (identity.IsEmpty()) + Log(LogInformation, "ApiListener") + << "No data received on new API connection. " + << "Ensure that the remote endpoints are properly configured in a cluster setup."; + else + Log(LogWarning, "ApiListener") + << "No data received on new API connection for identity '" << identity << "'. " + << "Ensure that the remote endpoints are properly configured in a cluster setup."; return; } diff --git a/lib/remote/apiuser.cpp b/lib/remote/apiuser.cpp index 183291af3..7bd953880 100644 --- a/lib/remote/apiuser.cpp +++ b/lib/remote/apiuser.cpp @@ -20,6 +20,7 @@ #include "remote/apiuser.hpp" #include "remote/apiuser-ti.cpp" #include "base/configtype.hpp" +#include "base/base64.hpp" using namespace icinga; @@ -34,3 +35,31 @@ ApiUser::Ptr ApiUser::GetByClientCN(const String& cn) return nullptr; } + +ApiUser::Ptr ApiUser::GetByAuthHeader(const String& auth_header) +{ + String::SizeType pos = auth_header.FindFirstOf(" "); + String username, password; + + if (pos != String::NPos && auth_header.SubStr(0, pos) == "Basic") { + String credentials_base64 = auth_header.SubStr(pos + 1); + String credentials = Base64::Decode(credentials_base64); + + String::SizeType cpos = credentials.FindFirstOf(":"); + + if (cpos != String::NPos) { + username = credentials.SubStr(0, cpos); + password = credentials.SubStr(cpos + 1); + } + } + + const ApiUser::Ptr& user = ApiUser::GetByName(username); + + /* Deny authentication if 1) given password is empty 2) configured password does not match. */ + if (password.IsEmpty()) + return nullptr; + else if (user && user->GetPassword() != password) + return nullptr; + + return user; +} diff --git a/lib/remote/apiuser.hpp b/lib/remote/apiuser.hpp index 755273bf4..15b1c41e3 100644 --- a/lib/remote/apiuser.hpp +++ b/lib/remote/apiuser.hpp @@ -36,6 +36,7 @@ public: DECLARE_OBJECTNAME(ApiUser); static ApiUser::Ptr GetByClientCN(const String& cn); + static ApiUser::Ptr GetByAuthHeader(const String& auth_header); }; } diff --git a/lib/remote/httpchunkedencoding.cpp b/lib/remote/httpchunkedencoding.cpp index e79d3483a..9981749c2 100644 --- a/lib/remote/httpchunkedencoding.cpp +++ b/lib/remote/httpchunkedencoding.cpp @@ -37,6 +37,8 @@ StreamReadStatus HttpChunkedEncoding::ReadChunkFromStream(const Stream::Ptr& str msgbuf << std::hex << line; msgbuf >> context.LengthIndicator; + if (context.LengthIndicator < 0) + BOOST_THROW_EXCEPTION(std::invalid_argument("HTTP chunk length must not be negative.")); } StreamReadContext& scontext = context.StreamContext; diff --git a/lib/remote/httprequest.cpp b/lib/remote/httprequest.cpp index 546728d66..a50cd3783 100644 --- a/lib/remote/httprequest.cpp +++ b/lib/remote/httprequest.cpp @@ -25,121 +25,149 @@ using namespace icinga; HttpRequest::HttpRequest(Stream::Ptr stream) - : Complete(false), + : CompleteHeaders(false), + CompleteHeaderCheck(false), + CompleteBody(false), ProtocolVersion(HttpVersion11), Headers(new Dictionary()), m_Stream(std::move(stream)), m_State(HttpRequestStart) { } -bool HttpRequest::Parse(StreamReadContext& src, bool may_wait) +bool HttpRequest::ParseHeaders(StreamReadContext& src, bool may_wait) { if (!m_Stream) return false; - if (m_State != HttpRequestBody) { - String line; - StreamReadStatus srs = m_Stream->ReadLine(&line, src, may_wait); + if (m_State != HttpRequestStart && m_State != HttpRequestHeaders) + BOOST_THROW_EXCEPTION(std::runtime_error("Invalid HTTP state")); + + String line; + StreamReadStatus srs = m_Stream->ReadLine(&line, src, may_wait); + + if (srs != StatusNewItem) { + if (src.Size > 512) + BOOST_THROW_EXCEPTION(std::invalid_argument("Line length for HTTP header exceeded")); + + return false; + } + + if (line.GetLength() > 512) + BOOST_THROW_EXCEPTION(std::invalid_argument("Line length for HTTP header exceeded")); + + if (m_State == HttpRequestStart) { + /* ignore trailing new-lines */ + if (line == "") + return true; + + std::vector tokens = line.Split(" "); + Log(LogDebug, "HttpRequest") + << "line: " << line << ", tokens: " << tokens.size(); + if (tokens.size() != 3) + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid HTTP request")); + + RequestMethod = tokens[0]; + RequestUrl = 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; + return true; + } else { // m_State = HttpRequestHeaders + if (line == "") { + m_State = HttpRequestBody; + CompleteHeaders = true; + return true; + + } else { + if (Headers->GetLength() > 128) + BOOST_THROW_EXCEPTION(std::invalid_argument("Maximum number of HTTP request headers exceeded")); + + 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); + + if (key == "x-http-method-override") + RequestMethod = value; + + return true; + } + } +} + +bool HttpRequest::ParseBody(StreamReadContext& src, bool may_wait) +{ + if (!m_Stream) + return false; + + if (m_State != HttpRequestBody) + BOOST_THROW_EXCEPTION(std::runtime_error("Invalid HTTP state")); + + /* we're done if the request doesn't contain a message body */ + if (!Headers->Contains("content-length") && !Headers->Contains("transfer-encoding")) { + CompleteBody = true; + return false; + } else if (!m_Body) + m_Body = new FIFO(); + + if (Headers->Get("transfer-encoding") == "chunked") { + if (!m_ChunkContext) + m_ChunkContext = std::make_shared(std::ref(src)); + + char *data; + size_t size; + StreamReadStatus srs = HttpChunkedEncoding::ReadChunkFromStream(m_Stream, &data, &size, *m_ChunkContext.get(), may_wait); if (srs != StatusNewItem) return false; - if (m_State == HttpRequestStart) { - /* ignore trailing new-lines */ - if (line == "") - return true; + m_Body->Write(data, size); - std::vector tokens = line.Split(" "); - Log(LogDebug, "HttpRequest") - << "line: " << line << ", tokens: " << tokens.size(); - if (tokens.size() != 3) - BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid HTTP request")); + delete [] data; - RequestMethod = tokens[0]; - RequestUrl = 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; - } 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(); - - 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); - - if (key == "x-http-method-override") - RequestMethod = value; - } - } else { - VERIFY(!"Invalid HTTP request state."); - } - } else if (m_State == HttpRequestBody) { - if (Headers->Get("transfer-encoding") == "chunked") { - if (!m_ChunkContext) - m_ChunkContext = std::make_shared(std::ref(src)); - - char *data; - size_t size; - StreamReadStatus srs = HttpChunkedEncoding::ReadChunkFromStream(m_Stream, &data, &size, *m_ChunkContext.get(), may_wait); - - if (srs != StatusNewItem) - return false; - - 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; + if (size == 0) { + CompleteBody = true; + return false; + } else return true; - } } + 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; + } + + long length_indicator_signed = Convert::ToLong(Headers->Get("content-length")); + + if (length_indicator_signed < 0) + BOOST_THROW_EXCEPTION(std::invalid_argument("Content-Length must not be negative.")); + + size_t length_indicator = length_indicator_signed; + + if (src.Size < length_indicator) { + src.MustRead = true; + return false; + } + + m_Body->Write(src.Buffer, length_indicator); + src.DropData(length_indicator); + CompleteBody = true; return true; } diff --git a/lib/remote/httprequest.hpp b/lib/remote/httprequest.hpp index b59917283..14e0677cd 100644 --- a/lib/remote/httprequest.hpp +++ b/lib/remote/httprequest.hpp @@ -52,7 +52,9 @@ enum HttpRequestState struct HttpRequest { public: - bool Complete; + bool CompleteHeaders; + bool CompleteHeaderCheck; + bool CompleteBody; String RequestMethod; Url::Ptr RequestUrl; @@ -62,7 +64,8 @@ public: HttpRequest(Stream::Ptr stream); - bool Parse(StreamReadContext& src, bool may_wait); + bool ParseHeaders(StreamReadContext& src, bool may_wait); + bool ParseBody(StreamReadContext& src, bool may_wait); size_t ReadBody(char *data, size_t count); void AddHeader(const String& key, const String& value); diff --git a/lib/remote/httpserverconnection.cpp b/lib/remote/httpserverconnection.cpp index 1b19635a4..e54653f22 100644 --- a/lib/remote/httpserverconnection.cpp +++ b/lib/remote/httpserverconnection.cpp @@ -24,12 +24,13 @@ #include "remote/apifunction.hpp" #include "remote/jsonrpc.hpp" #include "base/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/configtype.hpp" +#include "base/exception.hpp" +#include "base/logger.hpp" +#include "base/objectlock.hpp" +#include "base/timer.hpp" +#include "base/utility.hpp" #include using namespace icinga; @@ -52,7 +53,7 @@ void HttpServerConnection::StaticInitialize() { l_HttpServerConnectionTimeoutTimer = new Timer(); l_HttpServerConnectionTimeoutTimer->OnTimerExpired.connect(std::bind(&HttpServerConnection::TimeoutTimerHandler)); - l_HttpServerConnectionTimeoutTimer->SetInterval(15); + l_HttpServerConnectionTimeoutTimer->SetInterval(5); l_HttpServerConnectionTimeoutTimer->Start(); } @@ -76,6 +77,12 @@ TlsStream::Ptr HttpServerConnection::GetStream() const void HttpServerConnection::Disconnect() { + boost::mutex::scoped_try_lock lock(m_DataHandlerMutex); + if (!lock.owns_lock()) { + Log(LogInformation, "HttpServerConnection", "Unable to disconnect Http client, I/O thread busy"); + return; + } + Log(LogDebug, "HttpServerConnection", "Http client disconnected"); ApiListener::Ptr listener = ApiListener::GetInstance(); @@ -89,98 +96,137 @@ void HttpServerConnection::Disconnect() bool HttpServerConnection::ProcessMessage() { + bool res; + HttpResponse response(m_Stream, m_CurrentRequest); - try { - res = m_CurrentRequest.Parse(m_Context, false); - } catch (const std::invalid_argument& ex) { - HttpResponse response(m_Stream, m_CurrentRequest); - response.SetStatus(400, "Bad request"); - String msg = String("

Bad request

") + ex.what() + "

"; - response.WriteBody(msg.CStr(), msg.GetLength()); - response.Finish(); + if (!m_CurrentRequest.CompleteHeaders) { + try { + res = m_CurrentRequest.ParseHeaders(m_Context, false); + } catch (const std::invalid_argument& ex) { + response.SetStatus(400, "Bad Request"); + String msg = String("

Bad Request

") + ex.what() + "

"; + response.WriteBody(msg.CStr(), msg.GetLength()); + response.Finish(); - m_Stream->Shutdown(); - return false; - } catch (const std::exception& ex) { - HttpResponse response(m_Stream, m_CurrentRequest); - response.SetStatus(400, "Bad request"); - String msg = "

Bad request

" + DiagnosticInformation(ex) + "

"; - response.WriteBody(msg.CStr(), msg.GetLength()); - response.Finish(); + m_CurrentRequest.~HttpRequest(); + new (&m_CurrentRequest) HttpRequest(m_Stream); - m_Stream->Shutdown(); - return false; + m_Stream->Shutdown(); + + return false; + } catch (const std::exception& ex) { + response.SetStatus(500, "Internal Server Error"); + String msg = "

Internal Server Error

" + DiagnosticInformation(ex) + "

"; + response.WriteBody(msg.CStr(), msg.GetLength()); + response.Finish(); + + m_CurrentRequest.~HttpRequest(); + new (&m_CurrentRequest) HttpRequest(m_Stream); + + m_Stream->Shutdown(); + + return false; + } + return res; } - if (m_CurrentRequest.Complete) { - m_RequestQueue.Enqueue(std::bind(&HttpServerConnection::ProcessMessageAsync, - HttpServerConnection::Ptr(this), m_CurrentRequest)); + if (!m_CurrentRequest.CompleteHeaderCheck) { + m_CurrentRequest.CompleteHeaderCheck = true; + if (!ManageHeaders(response)) { + m_CurrentRequest.~HttpRequest(); + new (&m_CurrentRequest) HttpRequest(m_Stream); - m_Seen = Utility::GetTime(); - m_PendingRequests++; + m_Stream->Shutdown(); - m_CurrentRequest.~HttpRequest(); - new (&m_CurrentRequest) HttpRequest(m_Stream); - - return true; - } - - return res; -} - -void HttpServerConnection::ProcessMessageAsync(HttpRequest& request) -{ - String auth_header = request.Headers->Get("authorization"); - - String::SizeType pos = auth_header.FindFirstOf(" "); - String username, password; - - if (pos != String::NPos && auth_header.SubStr(0, pos) == "Basic") { - String credentials_base64 = auth_header.SubStr(pos + 1); - String credentials = Base64::Decode(credentials_base64); - - String::SizeType cpos = credentials.FindFirstOf(":"); - - if (cpos != String::NPos) { - username = credentials.SubStr(0, cpos); - password = credentials.SubStr(cpos + 1); + return false; } } - ApiUser::Ptr user; + if (!m_CurrentRequest.CompleteBody) { + try { + res = m_CurrentRequest.ParseBody(m_Context, false); + } catch (const std::invalid_argument& ex) { + response.SetStatus(400, "Bad Request"); + String msg = String("

Bad Request

") + ex.what() + "

"; + response.WriteBody(msg.CStr(), msg.GetLength()); + response.Finish(); + + m_CurrentRequest.~HttpRequest(); + new (&m_CurrentRequest) HttpRequest(m_Stream); + + m_Stream->Shutdown(); + + return false; + } catch (const std::exception& ex) { + response.SetStatus(500, "Internal Server Error"); + String msg = "

Internal Server Error

" + DiagnosticInformation(ex) + "

"; + response.WriteBody(msg.CStr(), msg.GetLength()); + response.Finish(); + + m_CurrentRequest.~HttpRequest(); + new (&m_CurrentRequest) HttpRequest(m_Stream); + + m_Stream->Shutdown(); + + return false; + } + return res; + } + + m_RequestQueue.Enqueue(std::bind(&HttpServerConnection::ProcessMessageAsync, + HttpServerConnection::Ptr(this), m_CurrentRequest, response, m_AuthenticatedUser)); + + m_Seen = Utility::GetTime(); + m_PendingRequests++; + + m_CurrentRequest.~HttpRequest(); + new (&m_CurrentRequest) HttpRequest(m_Stream); + + return false; +} + +bool HttpServerConnection::ManageHeaders(HttpResponse& response) +{ + static const size_t defaultContentLengthLimit = 1 * 1028 * 1028; + static const Dictionary::Ptr specialContentLengthLimits = new Dictionary({ + {"*", 512 * 1028 * 1028}, + {"config/modify", 512 * 1028 * 1028}, + {"console", 512 * 1028 * 1028}, + {"objects/create", 512 * 1028 * 1028}, + {"objects/modify", 512 * 1028 * 1028}, + {"objects/delete", 512 * 1028 * 1028} + }); + + if (m_CurrentRequest.Headers->Get("expect") == "100-continue") { + String continueResponse = "HTTP/1.1 100 Continue\r\n\r\n"; + m_Stream->Write(continueResponse.CStr(), continueResponse.GetLength()); + } /* client_cn matched. */ if (m_ApiUser) - user = m_ApiUser; - else { - user = ApiUser::GetByName(username); + m_AuthenticatedUser = m_ApiUser; + else + m_AuthenticatedUser = ApiUser::GetByAuthHeader(m_CurrentRequest.Headers->Get("authorization")); - /* Deny authentication if 1) given password is empty 2) configured password does not match. */ - if (password.IsEmpty()) - user.reset(); - else if (user && user->GetPassword() != password) - user.reset(); - } + String requestUrl = m_CurrentRequest.RequestUrl->Format(); - String requestUrl = request.RequestUrl->Format(); + Socket::Ptr socket = m_Stream->GetSocket(); Log(LogInformation, "HttpServerConnection") - << "Request: " << request.RequestMethod << " " << requestUrl - << " (from " << m_Stream->GetSocket()->GetPeerAddress() << ", user: " << (user ? user->GetName() : "") << ")"; - - HttpResponse response(m_Stream, request); + << "Request: " << m_CurrentRequest.RequestMethod << " " << requestUrl + << " (from " << (socket ? socket->GetPeerAddress() : "") + << ", user: " << (m_AuthenticatedUser ? m_AuthenticatedUser->GetName() : "") << ")"; ApiListener::Ptr listener = ApiListener::GetInstance(); if (!listener) - return; + return false; Array::Ptr headerAllowOrigin = listener->GetAccessControlAllowOrigin(); if (headerAllowOrigin->GetLength() != 0) { - String origin = request.Headers->Get("origin"); - + String origin = m_CurrentRequest.Headers->Get("origin"); { ObjectLock olock(headerAllowOrigin); @@ -193,9 +239,9 @@ void HttpServerConnection::ProcessMessageAsync(HttpRequest& request) if (listener->GetAccessControlAllowCredentials()) response.AddHeader("Access-Control-Allow-Credentials", "true"); - String accessControlRequestMethodHeader = request.Headers->Get("access-control-request-method"); + String accessControlRequestMethodHeader = m_CurrentRequest.Headers->Get("access-control-request-method"); - if (!accessControlRequestMethodHeader.IsEmpty()) { + if (m_CurrentRequest.RequestMethod == "OPTIONS" && !accessControlRequestMethodHeader.IsEmpty()) { response.SetStatus(200, "OK"); response.AddHeader("Access-Control-Allow-Methods", listener->GetAccessControlAllowMethods()); @@ -205,27 +251,27 @@ void HttpServerConnection::ProcessMessageAsync(HttpRequest& request) response.WriteBody(msg.CStr(), msg.GetLength()); response.Finish(); - m_PendingRequests--; - - return; + return false; } } - String accept_header = request.Headers->Get("accept"); - - if (request.RequestMethod != "GET" && accept_header != "application/json") { + if (m_CurrentRequest.RequestMethod != "GET" && m_CurrentRequest.Headers->Get("accept") != "application/json") { response.SetStatus(400, "Wrong Accept header"); response.AddHeader("Content-Type", "text/html"); String msg = "

Accept header is missing or not set to 'application/json'.

"; response.WriteBody(msg.CStr(), msg.GetLength()); - } else if (!user) { + response.Finish(); + return false; + } + + if (!m_AuthenticatedUser) { Log(LogWarning, "HttpServerConnection") - << "Unauthorized request: " << request.RequestMethod << " " << requestUrl; + << "Unauthorized request: " << m_CurrentRequest.RequestMethod << " " << requestUrl; response.SetStatus(401, "Unauthorized"); response.AddHeader("WWW-Authenticate", "Basic realm=\"Icinga 2\""); - if (request.Headers->Get("accept") == "application/json") { + if (m_CurrentRequest.Headers->Get("accept") == "application/json") { Dictionary::Ptr result = new Dictionary({ { "error", 401 }, { "status", "Unauthorized. Please check your user credentials." } @@ -237,32 +283,48 @@ void HttpServerConnection::ProcessMessageAsync(HttpRequest& request) String msg = "

Unauthorized. Please check your user credentials.

"; response.WriteBody(msg.CStr(), msg.GetLength()); } - } else { - try { - HttpHandler::ProcessRequest(user, request, response); - } catch (const std::exception& ex) { - Log(LogCritical, "HttpServerConnection") - << "Unhandled exception while processing Http request: " << DiagnosticInformation(ex); - response.SetStatus(503, "Unhandled exception"); - String errorInfo = DiagnosticInformation(ex); + response.Finish(); + return false; + } - if (request.Headers->Get("accept") == "application/json") { - Dictionary::Ptr result = new Dictionary({ - { "error", 503 }, - { "status", errorInfo } - }); + size_t maxSize = defaultContentLengthLimit; - HttpUtility::SendJsonBody(response, nullptr, result); - } else { - response.AddHeader("Content-Type", "text/plain"); - response.WriteBody(errorInfo.CStr(), errorInfo.GetLength()); - } - } + Array::Ptr permissions = m_AuthenticatedUser->GetPermissions(); + ObjectLock olock(permissions); + + for (const Value& permission : permissions) { + std::vector permissionParts = String(permission).Split("/"); + String permissionPath = permissionParts[0] + (permissionParts.size() > 1 ? "/" + permissionParts[1] : ""); + int size = specialContentLengthLimits->Get(permissionPath); + maxSize = size > maxSize ? size : maxSize; + } + + size_t contentLength = m_CurrentRequest.Headers->Get("content-length"); + + if (contentLength > maxSize) { + response.SetStatus(400, "Bad Request"); + String msg = String("

Content length exceeded maximum

"); + response.WriteBody(msg.CStr(), msg.GetLength()); + response.Finish(); + + return false; + } + + return true; +} + +void HttpServerConnection::ProcessMessageAsync(HttpRequest& request, HttpResponse& response, const ApiUser::Ptr& user) +{ + try { + HttpHandler::ProcessRequest(user, request, response); + } catch (const std::exception& ex) { + Log(LogCritical, "HttpServerConnection") + << "Unhandled exception while processing Http request: " << DiagnosticInformation(ex); + HttpUtility::SendJsonError(response, nullptr, 503, "Unhandled exception" , DiagnosticInformation(ex)); } response.Finish(); - m_PendingRequests--; } diff --git a/lib/remote/httpserverconnection.hpp b/lib/remote/httpserverconnection.hpp index f52110013..5f959b8a0 100644 --- a/lib/remote/httpserverconnection.hpp +++ b/lib/remote/httpserverconnection.hpp @@ -21,9 +21,9 @@ #define HTTPSERVERCONNECTION_H #include "remote/httprequest.hpp" +#include "remote/httpresponse.hpp" #include "remote/apiuser.hpp" #include "base/tlsstream.hpp" -#include "base/timer.hpp" #include "base/workqueue.hpp" namespace icinga @@ -51,6 +51,7 @@ public: private: ApiUser::Ptr m_ApiUser; + ApiUser::Ptr m_AuthenticatedUser; TlsStream::Ptr m_Stream; double m_Seen; HttpRequest m_CurrentRequest; @@ -67,7 +68,9 @@ private: static void TimeoutTimerHandler(); void CheckLiveness(); - void ProcessMessageAsync(HttpRequest& request); + bool ManageHeaders(HttpResponse& response); + + void ProcessMessageAsync(HttpRequest& request, HttpResponse& response, const ApiUser::Ptr&); }; } diff --git a/lib/remote/pkiutility.cpp b/lib/remote/pkiutility.cpp index 6b896dab1..20d9ca6c2 100644 --- a/lib/remote/pkiutility.cpp +++ b/lib/remote/pkiutility.cpp @@ -121,8 +121,10 @@ std::shared_ptr PkiUtility::FetchCert(const String& host, const String& po try { stream->Handshake(); - } catch (...) { - + } catch (const std::exception& ex) { + Log(LogCritical, "pki") + << "Client TLS handshake failed. (" << ex.what() << ")"; + return std::shared_ptr(); } return stream->GetPeerCertificate();