/* * (c) Copyright, Real-Time Innovations, 2022. * All rights reserved. * * No duplications, whole or partial, manual or electronic, may be made * without express written permission. Any such copies, or * revisions thereof, must display this notice unaltered. * This code contains trade secrets of Real-Time Innovations, Inc. */ #ifndef HTTP_CLIENT_CONNECTION_HPP #define HTTP_CLIENT_CONNECTION_HPP #include #include #include #include #include #include #include "rti/apputils/ServiceException.hpp" #include "rti/apputils/http/common/Response.hpp" #include "rti/apputils/http/common/HttpUtils.hpp" #include "rti/apputils/http/log/Log.hpp" #include "Request.hpp" namespace rti { namespace apputils { namespace http { namespace client { /** * @brief This class is a wrapper around the SSL configuration for the client. * */ struct SslConfig { SslConfig( bool the_enabled = false, std::string the_client_cert = "", std::string the_server_cert = "") : enabled(the_enabled), client_cert(the_client_cert), server_cert(the_server_cert) { } bool enabled; std::string client_cert; std::string server_cert; }; /** * @brief This class deal with actual HTTP client requests and * responses to and from CivetWeb. */ class Connection { public: /** * @brief Construct a new Connection object Note that since every request * reconnects the connection, we do not need to connect in the constructor * itself. * * @param host Hostname of the server * @param port Port of the server * @param ssl_config SSL configuration for the client */ Connection( const std::string& host = std::string("localhost"), std::uint16_t port = HTTP_SERVER_DEFAULT_PORT, SslConfig ssl_config = SslConfig()) : host_ { host }, port_ { port }, ssl_config_ { ssl_config } { } /** * @brief send an actual HTTP request to CivetWeb. It extracts all * the information from the request and constructs a valid HTTP request * string. * * @param request HTTP request to be sent to the server. */ void send_http_request(const Request& request) { connect(); // Send HTTP method and URL auto mg_result = mg_printf( connection_.get(), "%s %s", method_to_string(request.http_method()).c_str(), request.uri()); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); // Add URL params to URL if (request.uri_parameters().size() > 0) { mg_result = mg_printf(connection_.get(), "?"); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); for (const auto& uri_param : request.uri_parameters()) { std::array uri_param_value; uri_param_value.fill('\0'); // We need to encode the URI params in case they contain // spaces or any other character that may not be valid in a // URL mg_result = mg_url_encode( uri_param.second.c_str(), uri_param_value.data(), MAX_URI_LENGTH); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_url_encode); mg_result = mg_printf( connection_.get(), "%s=%s&", uri_param.first.c_str(), uri_param_value.data()); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); } } // Specify HTTP version mg_result = mg_printf(connection_.get(), " HTTP/%s\r\n", HTTP_VERSION); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); // Add user-defined headers for (const auto& header : request.headers()) { mg_result = mg_printf( connection_.get(), "%s: %s\r\n", header.first.c_str(), header.second.c_str()); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); } // Add default headers if not defined if (request.header("Content-Length") == nullptr) { mg_result = mg_printf( connection_.get(), "Content-Length: %llu\r\n", static_cast(request.body().length())); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); } if (request.header("Content-Type") == nullptr) { mg_result = mg_printf( connection_.get(), "Content-Type: %s\r\n", HTTP_PLAIN_TEXT_CONTENT_TYPE); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); } if (request.header("Cache-Control") == nullptr) { mg_result = mg_printf( connection_.get(), "Cache-Control: %s\r\n", HTTP_CACHE_CONTROL_NO_CACHE); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); } if (request.header("Date") == nullptr) { mg_result = mg_printf( connection_.get(), "Date: %s\r\n", HttpSettings::get_current_date().c_str()); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); } // Separate headers from body mg_result = mg_printf(connection_.get(), "\r\n"); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result > 0, msg_client_error_printf); // Send body of the HTTP request -- if empty it will not write anything mg_result = mg_printf(connection_.get(), "%s", request.body().c_str()); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( mg_result >= 0, msg_client_error_printf); } /** * @brief returns a Response based on the actual HTTP response received from * CivetWeb. It extracts all the information originally stored in the * mg_connection and transforms it into a valid Response object. * * @param timeout Number of milliseconds to wait for a response. A negative * value means wait indefinitely. * * @return Response object with the response from CivetWeb. */ Response get_http_response(int timeout = HTTP_CLIENT_TIMEOUT_MS) { std::array error_buffer; error_buffer[0] = '\0'; // Get CivetWeb's response. const auto result = mg_get_response( connection_.get(), error_buffer.data(), error_buffer.size(), timeout); // If we timeout trying to get the response, throw an exception. RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( result >= 0, error_buffer.data()); // Convert CivetWeb's response into a Response const mg_response_info *response_info = mg_get_response_info(connection_.get()); RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( response_info != nullptr, msg_client_error_get_response_info); return Response { static_cast(response_info->status_code), response_info->status_text, [&] { const auto header = mg_get_header(connection_.get(), "Content-Type"); return (header == nullptr) ? "" : header; }(), this->read_content(), [&] { std::unordered_map headers; for (int i = 0; i < response_info->num_headers; ++i) { headers.emplace( response_info->http_headers[i].name, response_info->http_headers[i].value); } return headers; }() }; } std::string read_content(std::size_t chunk_size = 256) { const auto response_info = mg_get_response_info(connection_.get()); if (response_info == nullptr) { RTI_THROW_HTTP_EXCEPTION( StatusCode::internal_server_error, &RTI_LOG_ANY_FAILURE_s, "Error reading content"); } if (response_info->content_length >= 0) { return read_content_with_content_length( connection_.get(), static_cast(response_info->content_length)); } return read_content_without_content_length( connection_.get(), chunk_size); } private: /** * @brief time we connect, we reset the mg_connection. This ensures * that CivetWeb's internal buffers and state is cleaned up. This makes * the Connection easier to use, since it does not need * to be reconnected with every unit test run. */ void connect() { std::array error_buffer; error_buffer[0] = 0; if (ssl_config_.enabled) { mg_client_options client_options = create_client_options(); connection_.reset(mg_connect_client_secure( &client_options, error_buffer.data(), HTTP_BODY_INITIAL_LENGTH)); } else { connection_.reset(mg_connect_client( host_.c_str(), port_, ssl_config_.enabled, error_buffer.data(), HTTP_BODY_INITIAL_LENGTH)); } RTI_SERVICE_CHECK_CONDITION_W_EXTRA_INFO( connection_ != nullptr || error_buffer[0] == 0, error_buffer.data()); } /** * @brief This method closes any existing connection. */ struct Disconnect { void operator()(mg_connection *connection) const { mg_close_connection(connection); } }; /** * @brief This method create a new mg_client_options object with the * information provided by the user. */ mg_client_options create_client_options() { mg_client_options client_options; client_options.host = host_.c_str(); client_options.port = port_; client_options.client_cert = (ssl_config_.client_cert.empty() || !ssl_config_.enabled) ? nullptr : ssl_config_.client_cert.c_str(); client_options.server_cert = (ssl_config_.server_cert.empty() || !ssl_config_.enabled) ? nullptr : ssl_config_.server_cert.c_str(); client_options.host_name = !ssl_config_.enabled ? nullptr : client_options.host; return client_options; } private: std::unique_ptr connection_; std::string host_; std::uint16_t port_; SslConfig ssl_config_; }; }}}} // namespace rti::apputils::http::client #endif // HTTP_CLIENT_CONNECTION_HPP