From f20a2c50d9ac3cecc3c0de2c3c4bf8a01536fcd2 Mon Sep 17 00:00:00 2001 From: "Eduardo Ramos Testillano (ert)" Date: Wed, 25 May 2022 02:35:34 +0200 Subject: [PATCH] Improve client connection classes - Add HEAD to client methods. - Add headers to send() method. - Add headers to response struct. - Improve getUri() to allow scheme configuration, but implementing a default behavior. Also, uri path is protected to allow starting with slash '/' or not. - Allow secure client connections. - Implement reconnect() method. - Implement connection asString() method. - Implement static headersAsString() method to be available on ert::http2comm::headersAsString(). Use it from Http2Headers class (asString() method). - Connect on send(). ToDo: manage timeout for waitToBeConnected(). --- include/ert/http2comm/Http2Client.hpp | 15 ++-- include/ert/http2comm/Http2Connection.hpp | 42 +++++++-- include/ert/http2comm/Http2Headers.hpp | 16 ++++ src/Http2Client.cpp | 98 +++++++++++++------- src/Http2Connection.cpp | 105 +++++++++++++++++----- src/Http2Headers.cpp | 19 ++++ 6 files changed, 223 insertions(+), 72 deletions(-) diff --git a/include/ert/http2comm/Http2Client.hpp b/include/ert/http2comm/Http2Client.hpp index 5eed07c..7d27f0d 100644 --- a/include/ert/http2comm/Http2Client.hpp +++ b/include/ert/http2comm/Http2Client.hpp @@ -84,13 +84,14 @@ class Http2Client enum class Method { - GET, PUT, POST, DELETE + POST, GET, PUT, DELETE, HEAD }; struct response { std::string body; - int status; //http result code + int statusCode; + nghttp2::asio_http2::header_map headers; }; private: @@ -118,16 +119,16 @@ class Http2Client virtual void setHttp2Connection(std::shared_ptr connection); virtual std::shared_ptr getHttp2Connection(); - virtual Http2Client::response send(const Http2Client::Method&, - const std::string& uri_path, - const std::string& json); + virtual Http2Client::response send(const Http2Client::Method &method, + const std::string &uri, + const std::string &body, + const nghttp2::asio_http2::header_map &headers); private: std::shared_ptr connection_; const std::chrono::milliseconds request_timeout_; const std::string host_; - const std::string scheme_; - std::string getUri(const std::string& uri_path); + std::string getUri(const std::string &uri, const std::string &scheme = "" /* http or https by default, but could be forced here */); }; } diff --git a/include/ert/http2comm/Http2Connection.hpp b/include/ert/http2comm/Http2Connection.hpp index 2b761f6..12cf2da 100644 --- a/include/ert/http2comm/Http2Connection.hpp +++ b/include/ert/http2comm/Http2Connection.hpp @@ -79,6 +79,12 @@ class Http2Connection CLOSED }; + // Seccion factory with io_service, host, port and secure inputs: + nghttp2::asio_http2::client::session createSession(boost::asio::io_service &ioService, const std::string &host, const std::string &port, bool secure); + + // Set on_connect/on_error session callbacks + void configureSession(); + public: /// Class constructors @@ -87,8 +93,9 @@ class Http2Connection * * \param host Endpoint host * \param port Endpoint port + * \param secure Secure connection. False by default */ - Http2Connection(const std::string& host, const std::string& port); + Http2Connection(const std::string& host, const std::string& port, bool secure); /** * Copy constructor @@ -150,6 +157,13 @@ class Http2Connection */ const std::string& getPort() const; + /** + * Returns true for secured connection + * + * \return Boolean about secured connection + */ + bool isSecure() const; + /** * Returns the connection status * @@ -184,6 +198,17 @@ class Http2Connection */ void onClose(connection_callback connection_closed_callback); + /** + * Reconnection procedure + */ + void reconnect(); + + + /** + * Class string representation + */ + std::string asString() const; + private: /// Internal methods @@ -200,20 +225,21 @@ class Http2Connection private: /// ASIO attributes - std::unique_ptr - io_service_; //non-copyable and non-movable - std::unique_ptr work_; - nghttp2::asio_http2::client::session session_; //non-copyable + boost::asio::io_service io_service_; + boost::asio::io_service::work work_; + nghttp2::asio_http2::client::session *session_; // session is non-copyable, so we will use this pointer + // to allow recreating the session (reconnect feature). /// Class attributes Status status_; - const std::string host_; - const std::string port_; + std::string host_; + std::string port_; + bool secure_; connection_callback connection_closed_callback_; /// Concurrency attributes std::mutex mutex_; - std::thread execution_; + std::thread thread_; std::condition_variable status_change_cond_var_; diff --git a/include/ert/http2comm/Http2Headers.hpp b/include/ert/http2comm/Http2Headers.hpp index db133aa..9445663 100644 --- a/include/ert/http2comm/Http2Headers.hpp +++ b/include/ert/http2comm/Http2Headers.hpp @@ -49,6 +49,17 @@ namespace ert namespace http2comm { +/** + * Prints headers list for traces. For example: + * '[content-length: 200][content-type: application/json; charset=utf-8]' + * + * @param headers nghttp2 headers map + * + * @return sorted query parameters URI part + */ +std::string headersAsString(const nghttp2::asio_http2::header_map &headers); + + class Http2Headers { nghttp2::asio_http2::header_map headers_{}; @@ -99,6 +110,11 @@ class Http2Headers * Gets current built header map */ const nghttp2::asio_http2::header_map& getHeaders() const; + + /** + * Class string representation + */ + std::string asString() const; }; } diff --git a/src/Http2Client.cpp b/src/Http2Client.cpp index 8f13cf3..ad30da6 100644 --- a/src/Http2Client.cpp +++ b/src/Http2Client.cpp @@ -43,20 +43,18 @@ SOFTWARE. #include +#include #include #include namespace { -std::map method_to_str = { { - ert::http2comm::Http2Client::Method::GET, "GET" - }, { - ert::http2comm::Http2Client::Method::PUT, "PUT" - }, { - ert::http2comm::Http2Client::Method::POST, "POST" - }, { - ert::http2comm::Http2Client::Method::DELETE, "DELETE" - } +std::map method_to_str = { + { ert::http2comm::Http2Client::Method::POST, "POST" }, + { ert::http2comm::Http2Client::Method::GET, "GET" }, + { ert::http2comm::Http2Client::Method::PUT, "PUT" }, + { ert::http2comm::Http2Client::Method::DELETE, "DELETE" }, + { ert::http2comm::Http2Client::Method::HEAD, "HEAD" } }; } @@ -67,14 +65,13 @@ namespace http2comm Http2Client::Http2Client(std::shared_ptr connection, const std::chrono::milliseconds& request_timeout) : - connection_(connection), request_timeout_(request_timeout), scheme_( - "http") + connection_(connection), request_timeout_(request_timeout) { } Http2Client::Http2Client(const std::chrono::milliseconds& request_timeout) : - request_timeout_(request_timeout), scheme_("http") + request_timeout_(request_timeout) { } @@ -92,40 +89,55 @@ Http2Client::getHttp2Connection() } Http2Client::response Http2Client::send( - const Http2Client::Method& - method, - const std::string& uri_path, const std::string& json) + const Http2Client::Method &method, + const std::string &uri, + const std::string &body, + const nghttp2::asio_http2::header_map &headers) { // Internal Server Error response if connection not initialized if (!connection_) { + LOGINFORMATIONAL(ert::tracing::Logger::informational("There must be a connection instance !", ERT_FILE_LOCATION)); return Http2Client::response{"", 500}; } + if (connection_->getStatus() != Http2Connection::Status::OPEN) + { + LOGINFORMATIONAL(ert::tracing::Logger::informational(ert::tracing::Logger::asString("Connection must be OPEN to send request (%s) !", connection_->asString().c_str()), ERT_FILE_LOCATION)); + + connection_->reconnect(); + connection_->waitToBeConnected(); + if (connection_->getStatus() != Http2Connection::Status::OPEN) { + LOGWARNING(ert::tracing::Logger::warning(ert::tracing::Logger::asString("Unable to reconnect '%s'", connection_->asString().c_str()), ERT_FILE_LOCATION)); + return Http2Client::response{"", 503}; + } + } - auto url = getUri(uri_path); + auto url = getUri(uri); auto method_str = ::method_to_str[method]; - LOGINFORMATIONAL(ert::tracing::Logger::informational( - ert::tracing::Logger::asString("Sending %s request to: %s; Data: %s; server connection status: %d", - method_str.c_str(), url.c_str(), json.c_str(), - static_cast(connection_->getStatus())), ERT_FILE_LOCATION)); + + LOGINFORMATIONAL( + ert::tracing::Logger::informational(ert::tracing::Logger::asString("Sending %s request to url: %s; body: %s; headers: %s; %s", + method_str.c_str(), url.c_str(), body.c_str(), headersAsString(headers).c_str(), connection_->asString().c_str()), ERT_FILE_LOCATION); + ); auto submit = [&, url](const nghttp2::asio_http2::client::session & sess, const nghttp2::asio_http2::header_map & headers, boost::system::error_code & ec) { - return sess.submit(ec, method_str, url, json, headers); + return sess.submit(ec, method_str, url, body, headers); }; auto& session = connection_->getSession(); auto task = std::make_shared(); - session.io_service().post([&, task] + session.io_service().post([&, task, headers] { boost::system::error_code ec; - //configure headers - nghttp2::asio_http2::header_value ctValue = {"application/json", 0}; - nghttp2::asio_http2::header_value clValue = {std::to_string(json.length()), 0}; - nghttp2::asio_http2::header_map headers = { {"content-type", ctValue}, {"content-length", clValue} }; + // // example to add headers: + // nghttp2::asio_http2::header_value ctValue = {"application/json", 0}; + // nghttp2::asio_http2::header_value clValue = {std::to_string(body.length()), 0}; + // headers.emplace("content-type", ctValue); + // headers.emplace("content-length", clValue); //perform submit auto req = submit(session, headers, ec); @@ -137,15 +149,15 @@ Http2Client::response Http2Client::send( { if (len > 0) { - std::string json (reinterpret_cast(data), len); - task->data += json; + std::string body (reinterpret_cast(data), len); + task->data += body; } else { //setting the value on 'response' (promise) will unlock 'done' (future) - task->response.set_value(Http2Client::response {task->data, res.status_code()}); + task->response.set_value(Http2Client::response {task->data, res.status_code(), res.header()}); LOGDEBUG(ert::tracing::Logger::debug(ert::tracing::Logger::asString( - "Request has been answered with %d; Data: %s", res.status_code(), task->data.c_str()), ERT_FILE_LOCATION)); + "Request has been answered with status code: %d; data: %s; headers: %s", res.status_code(), task->data.c_str(), headersAsString(res.header()).c_str()), ERT_FILE_LOCATION)); } }); }); @@ -170,10 +182,30 @@ Http2Client::response Http2Client::send( } -std::string Http2Client::getUri(const std::string& uri_path) +std::string Http2Client::getUri(const std::string& uri, const std::string &scheme) { - return scheme_ + "://" + connection_->getHost() + ":" - + connection_->getPort() + "/" + uri_path; + std::string result{}; + + if (scheme.empty()) { + result = "http"; + if (connection_->isSecure()) { + result += "s"; + } + } + else { + result = scheme; + } + + result += "://" + connection_->getHost() + ":" + connection_->getPort(); + if (uri.empty()) return result; + + if (uri[0] != '/') { + result += "/"; + } + + result += uri; + + return result; } } diff --git a/src/Http2Connection.cpp b/src/Http2Connection.cpp index 5703b2c..891f02d 100644 --- a/src/Http2Connection.cpp +++ b/src/Http2Connection.cpp @@ -42,38 +42,60 @@ SOFTWARE. #include #include + +namespace { +std::map status_to_str = { + { ert::http2comm::Http2Connection::Status::NOT_OPEN, "NOT_OPEN" }, + { ert::http2comm::Http2Connection::Status::OPEN, "OPEN" }, + { ert::http2comm::Http2Connection::Status::CLOSED, "CLOSED" } +}; +} + + namespace ert { namespace http2comm { -Http2Connection::Http2Connection(const std::string& host, - const std::string& port) : - io_service_(new boost::asio::io_service()), - work_(new boost::asio::io_service::work(*io_service_)), - session_(nghttp2::asio_http2::client::session(*io_service_, host, port)), - status_(Status::NOT_OPEN), - host_(host), - port_(port) -{ - session_.on_connect([this](boost::asio::ip::tcp::resolver::iterator endpoint_it) +nghttp2::asio_http2::client::session Http2Connection::createSession(boost::asio::io_service &ioService, const std::string &host, const std::string &port, bool secure) { + if (secure) { + boost::system::error_code ec; + boost::asio::ssl::context tls_ctx(boost::asio::ssl::context::sslv23); + tls_ctx.set_default_verify_paths(); + nghttp2::asio_http2::client::configure_tls_context(ec, tls_ctx); + return nghttp2::asio_http2::client::session(ioService, tls_ctx, host, port); + } - LOGINFORMATIONAL(ert::tracing::Logger::informational(ert::tracing::Logger::asString( - "Connected to %s:%s", host_.c_str(), port_.c_str()), ERT_FILE_LOCATION)); + return nghttp2::asio_http2::client::session(ioService, host, port); +} + +void Http2Connection::configureSession() { + session_->on_connect([this](boost::asio::ip::tcp::resolver::iterator endpoint_it) + { status_ = Status::OPEN; + LOGINFORMATIONAL(ert::tracing::Logger::informational(ert::tracing::Logger::asString("Connected to '%s'", asString().c_str()), ERT_FILE_LOCATION)); status_change_cond_var_.notify_one(); - }); - session_.on_error([this](const boost::system::error_code & ec) + session_->on_error([this](const boost::system::error_code & ec) { - LOGINFORMATIONAL(ert::tracing::Logger::informational(ert::tracing::Logger::asString( - "Error in the connection to %s:%s : %s", host_.c_str(), port_.c_str(), ec.message().c_str()), ERT_FILE_LOCATION)); - notifyClose(); + LOGINFORMATIONAL(ert::tracing::Logger::informational(ert::tracing::Logger::asString("Error on '%s'", asString().c_str()), ERT_FILE_LOCATION)); }); +} - execution_ = std::thread([&] {io_service_->run();}); +Http2Connection::Http2Connection(const std::string& host, + const std::string& port, + bool secure) : + work_(boost::asio::io_service::work(io_service_)), + status_(Status::NOT_OPEN), + host_(host), + port_(port), + secure_(secure), + session_(new nghttp2::asio_http2::client::session(createSession(io_service_, host, port, secure))) +{ + configureSession(); + thread_ = std::thread([&] { io_service_.run(); }); } Http2Connection::~Http2Connection() @@ -95,18 +117,19 @@ void Http2Connection::notifyClose() void Http2Connection::closeImpl() { - work_.reset(); - io_service_->stop(); + io_service_.stop(); - if (execution_.joinable()) + if (thread_.joinable()) { - execution_.join(); + thread_.join(); } if (status_ == Status::OPEN) { - session_.shutdown(); + if(session_) session_->shutdown(); } + + delete(session_); } void Http2Connection::close() @@ -117,7 +140,7 @@ void Http2Connection::close() nghttp2::asio_http2::client::session& Http2Connection::getSession() { - return session_; + return *session_; } const std::string& Http2Connection::getHost() const @@ -130,6 +153,11 @@ const std::string& Http2Connection::getPort() const return port_; } +bool Http2Connection::isSecure() const +{ + return secure_; +} + const Http2Connection::Status& Http2Connection::getStatus() const @@ -139,6 +167,8 @@ const bool Http2Connection::waitToBeConnected() { + LOGDEBUG(ert::tracing::Logger::debug(ert::tracing::Logger::asString("waitToBeConnected() to '%s'", asString().c_str()), ERT_FILE_LOCATION)); + std::unique_lock lock(mutex_); status_change_cond_var_.wait(lock, [&] { @@ -150,6 +180,8 @@ bool Http2Connection::waitToBeConnected() bool Http2Connection::waitToBeDisconnected(const std::chrono::duration& time) { + LOGDEBUG(ert::tracing::Logger::debug(ert::tracing::Logger::asString("waitToBeDisconnected() from '%s'", asString().c_str()), ERT_FILE_LOCATION)); + std::unique_lock lock(mutex_); return status_change_cond_var_.wait_for(lock, time, [&] { @@ -163,6 +195,31 @@ void Http2Connection::onClose(connection_callback connection_closed_callback_ = connection_closed_callback; } +void Http2Connection::reconnect() +{ + LOGINFORMATIONAL(ert::tracing::Logger::informational(ert::tracing::Logger::asString("Reconnecting to '%s'", asString().c_str()), ERT_FILE_LOCATION)); + + std::unique_lock lock(mutex_); // consider shared_mutex + delete(session_); + status_ = Status::NOT_OPEN; + session_ = new nghttp2::asio_http2::client::session(createSession(io_service_, host_, port_, secure_)); + configureSession(); +} + +std::string Http2Connection::asString() const { + std::string result{}; + + result += (secure_ ? "secured":"regular"); + result += " connection | host: "; + result += host_; + result += " | port: "; + result += port_; + result += " | status: "; + result += ::status_to_str[getStatus()]; + + return result; +} + } } diff --git a/src/Http2Headers.cpp b/src/Http2Headers.cpp index 4d5cb2f..fd0d875 100644 --- a/src/Http2Headers.cpp +++ b/src/Http2Headers.cpp @@ -46,6 +46,21 @@ namespace ert namespace http2comm { +std::string headersAsString(const nghttp2::asio_http2::header_map &headers) { + std::string result = ""; + + for(auto it = headers.begin(); it != headers.end(); it ++) { + result += "["; + result += it->first; + result += ": "; + result += it->second.value; + result += "]"; + } + + return result; +} + + const nghttp2::asio_http2::header_map& Http2Headers::getHeaders() const { return headers_; } @@ -90,6 +105,10 @@ void Http2Headers::addContentType(const std::string& value, const std::string& h emplace(header, value); } +std::string Http2Headers::asString() const { + return headersAsString(headers_); +} + } }