From d340953bc925ff8535c5a8fac54db24b243ba8ad Mon Sep 17 00:00:00 2001 From: AppaRao Puli Date: Mon, 19 Oct 2020 13:21:42 +0530 Subject: [PATCH] EventService: https client support Add https client support for push style eventing. Using this BMC can push the event logs/telemetry data to event listener over secure http channel. Tested: - Created subscription with https destination url. Using SubmitTestEvent action set the event and can see event on event listener. - Validator passed. Change-Id: I44c3918b39baa2eb5fddda9d635f99aa280a422a Signed-off-by: AppaRao Puli Signed-off-by: P Dheeraj Srujan Kumar --- http/http_client.hpp | 370 +++++++++++++----- .../include/event_service_manager.hpp | 2 +- 2 files changed, 267 insertions(+), 105 deletions(-) diff --git a/http/http_client.hpp b/http/http_client.hpp index 5c7b13f..d782dee 100644 --- a/http/http_client.hpp +++ b/http/http_client.hpp @@ -31,12 +31,17 @@ namespace crow { static constexpr uint8_t maxRequestQueueSize = 50; +static constexpr unsigned int httpReadBodyLimit = 1024; enum class ConnState { initialized, + resolveInProgress, + resolveFailed, + resolved, connectInProgress, connectFailed, + sslHandshakeInProgress, connected, sendInProgress, sendFailed, @@ -50,53 +55,124 @@ enum class ConnState class HttpClient : public std::enable_shared_from_this { private: + boost::asio::ip::tcp::resolver resolver; + boost::asio::ssl::context ctx{boost::asio::ssl::context::tlsv12_client}; boost::beast::tcp_stream conn; + std::optional> sslConn; boost::asio::steady_timer timer; - boost::beast::flat_buffer buffer; + boost::beast::flat_static_buffer buffer; + std::optional< + boost::beast::http::response_parser> + parser; boost::beast::http::request req; - boost::beast::http::response res; boost::asio::ip::tcp::resolver::results_type endpoint; - std::vector> headers; + boost::beast::http::fields fields; std::queue requestDataQueue; - ConnState state; std::string subId; std::string host; std::string port; std::string uri; + bool useSsl; uint32_t retryCount; uint32_t maxRetryAttempts; uint32_t retryIntervalSecs; std::string retryPolicyAction; bool runningTimer; + ConnState state; + + void doResolve() + { + BMCWEB_LOG_DEBUG << "Trying to resolve: " << host << ":" << port; + if (state == ConnState::resolveInProgress) + { + return; + } + state = ConnState::resolveInProgress; + // TODO: Use async_resolver. boost asio example + // code as is crashing with async_resolve(). + try + { + endpoint = resolver.resolve(host, port); + } + catch (const std::exception& e) + { + BMCWEB_LOG_ERROR << "Failed to resolve hostname: " << host << " - " + << e.what(); + state = ConnState::resolveFailed; + checkQueue(); + return; + } + state = ConnState::resolved; + checkQueue(); + } void doConnect() { - if (state == ConnState::connectInProgress) + if (useSsl) + { + sslConn.emplace(conn, ctx); + } + + if ((state == ConnState::connectInProgress) || + (state == ConnState::sslHandshakeInProgress)) { return; } state = ConnState::connectInProgress; BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" << port; - // Set a timeout on the operation + + auto respHandler = + [self(shared_from_this())](const boost::beast::error_code ec, + const boost::asio::ip::tcp::resolver:: + results_type::endpoint_type& ep) { + if (ec) + { + BMCWEB_LOG_ERROR << "Connect " << ep + << " failed: " << ec.message(); + self->state = ConnState::connectFailed; + self->checkQueue(); + return; + } + BMCWEB_LOG_DEBUG << "Connected to: " << ep; + if (self->sslConn) + { + self->performHandshake(); + } + else + { + self->state = ConnState::connected; + self->checkQueue(); + } + }; + conn.expires_after(std::chrono::seconds(30)); - conn.async_connect(endpoint, [self(shared_from_this())]( - const boost::beast::error_code& ec, - const boost::asio::ip::tcp::resolver:: - results_type::endpoint_type& ep) { - if (ec) - { - BMCWEB_LOG_ERROR << "Connect " << ep - << " failed: " << ec.message(); - self->state = ConnState::connectFailed; - self->checkQueue(); - return; - } - self->state = ConnState::connected; - BMCWEB_LOG_DEBUG << "Connected to: " << ep; + conn.async_connect(endpoint, std::move(respHandler)); + } + + void performHandshake() + { + if (state == ConnState::sslHandshakeInProgress) + { + return; + } + state = ConnState::sslHandshakeInProgress; + + sslConn->async_handshake( + boost::asio::ssl::stream_base::client, + [self(shared_from_this())](const boost::beast::error_code ec) { + if (ec) + { + BMCWEB_LOG_ERROR << "SSL handshake failed: " + << ec.message(); + self->doCloseAndCheckQueue(ConnState::connectFailed); + return; + } + self->state = ConnState::connected; + BMCWEB_LOG_DEBUG << "SSL Handshake successfull"; - self->checkQueue(); - }); + self->checkQueue(); + }); } void sendMessage(const std::string& data) @@ -107,100 +183,170 @@ class HttpClient : public std::enable_shared_from_this } state = ConnState::sendInProgress; - BMCWEB_LOG_DEBUG << __FUNCTION__ << "(): " << host << ":" << port; + BMCWEB_LOG_DEBUG << host << ":" << port; - req.version(static_cast(11)); // HTTP 1.1 - req.target(uri); - req.method(boost::beast::http::verb::post); - - // Set headers - for (const auto& [key, value] : headers) + req = {}; + for (const auto& field : fields) { - req.set(key, value); + req.set(field.name_string(), field.value()); } req.set(boost::beast::http::field::host, host); + req.set(boost::beast::http::field::content_type, "text/plain"); + + req.version(static_cast(11)); // HTTP 1.1 + req.target(uri); + req.method(boost::beast::http::verb::post); req.keep_alive(true); req.body() = data; req.prepare_payload(); - // Set a timeout on the operation - conn.expires_after(std::chrono::seconds(30)); + auto respHandler = [self(shared_from_this())]( + const boost::beast::error_code ec, + const std::size_t& bytesTransferred) { + if (ec) + { + BMCWEB_LOG_ERROR << "sendMessage() failed: " << ec.message(); + self->doCloseAndCheckQueue(ConnState::sendFailed); + return; + } + BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: " + << bytesTransferred; + boost::ignore_unused(bytesTransferred); - // Send the HTTP request to the remote host - boost::beast::http::async_write( - conn, req, - [self(shared_from_this())](const boost::beast::error_code& ec, - const std::size_t& bytesTransferred) { - if (ec) - { - BMCWEB_LOG_ERROR << "sendMessage() failed: " - << ec.message(); - self->state = ConnState::sendFailed; - self->checkQueue(); - return; - } - BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: " - << bytesTransferred; - boost::ignore_unused(bytesTransferred); + self->recvMessage(); + }; - self->recvMessage(); - }); + conn.expires_after(std::chrono::seconds(30)); + if (sslConn) + { + boost::beast::http::async_write(*sslConn, req, + std::move(respHandler)); + } + else + { + boost::beast::http::async_write(conn, req, std::move(respHandler)); + } } void recvMessage() { - // Receive the HTTP response - boost::beast::http::async_read( - conn, buffer, res, - [self(shared_from_this())](const boost::beast::error_code& ec, - const std::size_t& bytesTransferred) { + auto respHandler = [self(shared_from_this())]( + const boost::beast::error_code ec, + const std::size_t& bytesTransferred) { + if (ec && ec != boost::beast::http::error::partial_message) + { + BMCWEB_LOG_ERROR << "recvMessage() failed: " << ec.message(); + self->doCloseAndCheckQueue(ConnState::recvFailed); + return; + } + BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: " + << bytesTransferred; + boost::ignore_unused(bytesTransferred); + + // TODO: check for return status code and perform + // retry if fails(Ex: 40x). Take action depending on + // retry policy. + BMCWEB_LOG_DEBUG << "recvMessage() data: " + << self->parser->get().body(); + + // Send is successful, Lets remove data from queue + // check for next request data in queue. + self->requestDataQueue.pop(); + + // Transfer ownership of the response + self->parser->release(); + + // TODO: Implement the keep-alive connections. + // Most of the web servers close connection abruptly + // and might be reason due to which its observed that + // stream_truncated(Next read) or partial_message + // errors. So for now, closing connection and re-open + // for all cases. + self->doCloseAndCheckQueue(ConnState::closed); + }; + + parser.emplace(std::piecewise_construct, std::make_tuple()); + parser->body_limit(httpReadBodyLimit); + // Since these are all push style eventing, we are not + // bothered about response parsing. + parser->skip(true); + buffer.consume(buffer.size()); + + conn.expires_after(std::chrono::seconds(30)); + if (sslConn) + { + boost::beast::http::async_read(*sslConn, buffer, *parser, + std::move(respHandler)); + } + else + { + boost::beast::http::async_read(conn, buffer, *parser, + std::move(respHandler)); + } + } + + void doCloseAndCheckQueue(const ConnState setState = ConnState::closed) + { + if (sslConn) + { + conn.expires_after(std::chrono::seconds(30)); + sslConn->async_shutdown([self = shared_from_this(), + setState{std::move(setState)}]( + const boost::system::error_code ec) { if (ec) { - BMCWEB_LOG_ERROR << "recvMessage() failed: " - << ec.message(); - self->state = ConnState::recvFailed; - self->checkQueue(); - return; + // Many https server closes connection abruptly + // i.e witnout close_notify. More details are at + // https://github.com/boostorg/beast/issues/824 + if (ec == boost::asio::ssl::error::stream_truncated) + { + BMCWEB_LOG_ERROR + << "doCloseAndCheckQueue(): Connection " + "closed by server. "; + } + else + { + BMCWEB_LOG_ERROR << "doCloseAndCheckQueue() failed: " + << ec.message(); + } } - BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: " - << bytesTransferred; - boost::ignore_unused(bytesTransferred); - - // Discard received data. We are not interested. - BMCWEB_LOG_DEBUG << "recvMessage() data: " << self->res; - - // Send is successful, Lets remove data from queue - // check for next request data in queue. - self->requestDataQueue.pop(); - self->state = ConnState::idle; + else + { + BMCWEB_LOG_DEBUG << "Connection closed gracefully..."; + } + self->conn.cancel(); + self->state = setState; self->checkQueue(); }); - } - - void doClose() - { - boost::beast::error_code ec; - conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); - - state = ConnState::closed; - // not_connected happens sometimes so don't bother reporting it. - if (ec && ec != boost::beast::errc::not_connected) + } + else { - BMCWEB_LOG_ERROR << "shutdown failed: " << ec.message(); - return; + boost::beast::error_code ec; + conn.expires_after(std::chrono::seconds(30)); + conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, + ec); + if (ec) + { + BMCWEB_LOG_ERROR << "doCloseAndCheckQueue() failed: " + << ec.message(); + } + else + { + BMCWEB_LOG_DEBUG << "Connection closed gracefully..."; + } + + conn.close(); + state = setState; + checkQueue(); } - BMCWEB_LOG_DEBUG << "Connection closed gracefully"; + return; } void checkQueue(const bool newRecord = false) { if (requestDataQueue.empty()) { - // TODO: Having issue in keeping connection alive. So lets close if - // nothing to be transferred. - doClose(); - BMCWEB_LOG_DEBUG << "requestDataQueue is empty\n"; return; } @@ -232,6 +378,7 @@ class HttpClient : public std::enable_shared_from_this } if ((state == ConnState::connectFailed) || + (state == ConnState::resolveFailed) || (state == ConnState::sendFailed) || (state == ConnState::recvFailed)) { @@ -256,14 +403,18 @@ class HttpClient : public std::enable_shared_from_this << " seconds. RetryCount = " << retryCount; timer.expires_after(std::chrono::seconds(retryIntervalSecs)); timer.async_wait( - [self = shared_from_this()](const boost::system::error_code&) { + [self = shared_from_this()](boost::system::error_code) { self->runningTimer = false; self->connStateCheck(); }); return; } - // reset retry count. - retryCount = 0; + + if (state == ConnState::idle) + { + // State idle means, previous attempt is successful. + retryCount = 0; + } connStateCheck(); return; @@ -273,15 +424,21 @@ class HttpClient : public std::enable_shared_from_this { switch (state) { + case ConnState::initialized: + case ConnState::resolveFailed: + case ConnState::connectFailed: + doResolve(); + break; case ConnState::connectInProgress: + case ConnState::resolveInProgress: + case ConnState::sslHandshakeInProgress: case ConnState::sendInProgress: case ConnState::suspended: case ConnState::terminated: // do nothing break; - case ConnState::initialized: case ConnState::closed: - case ConnState::connectFailed: + case ConnState::resolved: case ConnState::sendFailed: case ConnState::recvFailed: { @@ -297,22 +454,22 @@ class HttpClient : public std::enable_shared_from_this sendMessage(data); break; } + default: + break; } } public: explicit HttpClient(boost::asio::io_context& ioc, const std::string& id, const std::string& destIP, const std::string& destPort, - const std::string& destUri) : - conn(ioc), - timer(ioc), subId(id), host(destIP), port(destPort), uri(destUri), - retryCount(0), maxRetryAttempts(5), retryIntervalSecs(0), - retryPolicyAction("TerminateAfterRetries"), runningTimer(false) - { - boost::asio::ip::tcp::resolver resolver(ioc); - endpoint = resolver.resolve(host, port); - state = ConnState::initialized; - } + const std::string& destUri, + const bool inUseSsl = true) : + resolver(ioc), + conn(ioc), timer(ioc), subId(id), host(destIP), port(destPort), + uri(destUri), useSsl(inUseSsl), retryCount(0), maxRetryAttempts(5), + retryPolicyAction("TerminateAfterRetries"), runningTimer(false), + state(ConnState::initialized) + {} void sendData(const std::string& data) { @@ -337,7 +494,12 @@ class HttpClient : public std::enable_shared_from_this void setHeaders( const std::vector>& httpHeaders) { - headers = httpHeaders; + // Set headers + for (const auto& [key, value] : httpHeaders) + { + // TODO: Validate the header fileds before assign. + fields.set(key, value); + } } void setRetryConfig(const uint32_t retryAttempts, diff --git a/redfish-core/include/event_service_manager.hpp b/redfish-core/include/event_service_manager.hpp index 54dafb4..f68ae1d 100644 --- a/redfish-core/include/event_service_manager.hpp +++ b/redfish-core/include/event_service_manager.hpp @@ -387,7 +387,7 @@ class Subscription { conn = std::make_shared( crow::connections::systemBus->get_io_context(), id, host, port, - path); + path, (uriProto == "https" ? true : false)); } Subscription(const std::shared_ptr& adaptor) : -- 2.17.1