From 94fe1b2292b8481fca4d38a5797888ba4e37bf63 Mon Sep 17 00:00:00 2001 From: Noah Hilverling Date: Thu, 27 Jul 2017 14:57:34 +0200 Subject: [PATCH] HttpServerConnection: Implement CORS support fixes #4326 --- doc/09-object-types.md | 30 ++++++++++++---------- lib/remote/apilistener.ti | 17 ++++++++++++ lib/remote/httpresponse.cpp | 12 ++++----- lib/remote/httpresponse.hpp | 2 ++ lib/remote/httpserverconnection.cpp | 40 +++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 20 deletions(-) diff --git a/doc/09-object-types.md b/doc/09-object-types.md index 70d0eaaee..80810c7c7 100644 --- a/doc/09-object-types.md +++ b/doc/09-object-types.md @@ -43,19 +43,23 @@ object ApiListener "api" { Configuration Attributes: - Name |Description - --------------------------|-------------------------- - cert\_path |**Required.** Path to the public key. - key\_path |**Required.** Path to the private key. - ca\_path |**Required.** Path to the CA certificate file. - ticket\_salt |**Optional.** Private key for auto-signing. **Required** for a signing master instance. - crl\_path |**Optional.** Path to the CRL file. - bind\_host |**Optional.** The IP address the api listener should be bound to. Defaults to `0.0.0.0`. - bind\_port |**Optional.** The port the api listener should be bound to. Defaults to `5665`. - accept\_config |**Optional.** Accept zone configuration. Defaults to `false`. - accept\_commands |**Optional.** Accept remote commands. Defaults to `false`. - cipher\_list |**Optional.** Cipher list that is allowed. - tls\_protocolmin |**Optional.** Minimum TLS protocol version. Must be one of `TLSv1`, `TLSv1.1` or `TLSv1.2`. Defaults to `TLSv1`. + Name |Description + --------------------------------------|-------------------------------------- + cert\_path |**Required.** Path to the public key. + key\_path |**Required.** Path to the private key. + ca\_path |**Required.** Path to the CA certificate file. + ticket\_salt |**Optional.** Private key for auto-signing. **Required** for a signing master instance. + crl\_path |**Optional.** Path to the CRL file. + bind\_host |**Optional.** The IP address the api listener should be bound to. Defaults to `0.0.0.0`. + bind\_port |**Optional.** The port the api listener should be bound to. Defaults to `5665`. + accept\_config |**Optional.** Accept zone configuration. Defaults to `false`. + accept\_commands |**Optional.** Accept remote commands. Defaults to `false`. + cipher\_list |**Optional.** Cipher list that is allowed. + tls\_protocolmin |**Optional.** Minimum TLS protocol version. Must be one of `TLSv1`, `TLSv1.1` or `TLSv1.2`. Defaults to `TLSv1`. + access\_control\_allow\_origin |**Optional.** Specifies an array of origin URLs that may access the API. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Origin) + access\_control\_allow\_credentials |**Optional.** Indicates whether or not the actual request can be made using credentials. Defaults to `true`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Credentials) + access\_control\_allow\_headers |**Optional.** Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request. Defaults to `Authorization`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Headers) + access\_control\_allow\_methods |**Optional.** Used in response to a preflight request to indicate which HTTP methods can be used when making the actual request. Defaults to `GET, POST, PUT, DELETE`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Methods) ## ApiUser diff --git a/lib/remote/apilistener.ti b/lib/remote/apilistener.ti index 45e8bd036..ce91a3f03 100644 --- a/lib/remote/apilistener.ti +++ b/lib/remote/apilistener.ti @@ -49,6 +49,23 @@ class ApiListener : ConfigObject [config] String ticket_salt; + [config] array(String) access_control_allow_origin { + default {{{ return new Array(); }}} + }; + [config] bool access_control_allow_credentials + { + default {{{ return true; }}} + }; + [config] String access_control_allow_headers + { + default {{{ return "Authorization"; }}} + }; + [config] String access_control_allow_methods + { + default {{{ return "GET, POST, PUT, DELETE"; }}} + }; + + [state, no_user_modify] Timestamp log_message_timestamp; [no_user_modify] String identity; diff --git a/lib/remote/httpresponse.cpp b/lib/remote/httpresponse.cpp index d8c4e0997..7f77ed386 100644 --- a/lib/remote/httpresponse.cpp +++ b/lib/remote/httpresponse.cpp @@ -58,13 +58,7 @@ void HttpResponse::SetStatus(int code, const String& message) void HttpResponse::AddHeader(const String& key, const String& value) { - if (m_State != HttpResponseHeaders) { - Log(LogWarning, "HttpResponse", "Tried to add header after headers had already been sent."); - return; - } - - String header = key + ": " + value + "\r\n"; - m_Stream->Write(header.CStr(), header.GetLength()); + m_Headers.push_back(key + ": " + value + "\r\n"); } void HttpResponse::FinishHeaders(void) @@ -74,6 +68,10 @@ void HttpResponse::FinishHeaders(void) AddHeader("Transfer-Encoding", "chunked"); AddHeader("Server", "Icinga/" + Application::GetAppVersion()); + + for (const String& header : m_Headers) + m_Stream->Write(header.CStr(), header.GetLength()); + m_Stream->Write("\r\n", 2); m_State = HttpResponseBody; } diff --git a/lib/remote/httpresponse.hpp b/lib/remote/httpresponse.hpp index 0bd686292..da6afd66b 100644 --- a/lib/remote/httpresponse.hpp +++ b/lib/remote/httpresponse.hpp @@ -23,6 +23,7 @@ #include "remote/httprequest.hpp" #include "base/stream.hpp" #include "base/fifo.hpp" +#include namespace icinga { @@ -70,6 +71,7 @@ private: const HttpRequest& m_Request; Stream::Ptr m_Stream; FIFO::Ptr m_Body; + std::vector m_Headers; void FinishHeaders(void); }; diff --git a/lib/remote/httpserverconnection.cpp b/lib/remote/httpserverconnection.cpp index aac35aaa1..c2538b6eb 100644 --- a/lib/remote/httpserverconnection.cpp +++ b/lib/remote/httpserverconnection.cpp @@ -171,6 +171,46 @@ void HttpServerConnection::ProcessMessageAsync(HttpRequest& request) HttpResponse response(m_Stream, request); + ApiListener::Ptr listener = ApiListener::GetInstance(); + + if (!listener) + return; + + Array::Ptr headerAllowOrigin = listener->GetAccessControlAllowOrigin(); + + if (headerAllowOrigin->GetLength() != 0) { + String origin = request.Headers->Get("origin"); + + { + ObjectLock olock(headerAllowOrigin); + + for (const String& allowedOrigin : headerAllowOrigin) { + if (allowedOrigin == origin) + response.AddHeader("Access-Control-Allow-Origin", origin); + } + } + + if (listener->GetAccessControlAllowCredentials()) + response.AddHeader("Access-Control-Allow-Credentials", "true"); + + String accessControlRequestMethodHeader = request.Headers->Get("access-control-request-method"); + + if (!accessControlRequestMethodHeader.IsEmpty()) { + response.SetStatus(200, "OK"); + + response.AddHeader("Access-Control-Allow-Methods", listener->GetAccessControlAllowMethods()); + response.AddHeader("Access-Control-Allow-Headers", listener->GetAccessControlAllowHeaders()); + + String msg = "Preflight OK"; + response.WriteBody(msg.CStr(), msg.GetLength()); + + response.Finish(); + m_PendingRequests--; + + return; + } + } + String accept_header = request.Headers->Get("accept"); if (request.RequestMethod != "GET" && accept_header != "application/json") {