Files
qt-migration-legatus/qt6-after/qt6-migrated-legatus/include/rti/apputils/http/client/Connection.hpp
T

356 lines
12 KiB
C++

/*
* (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 <array>
#include <chrono>
#include <memory>
#include <string>
#include <thread>
#include <civetweb.h>
#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<char, MAX_URI_LENGTH> 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<unsigned long long>(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<char, HTTP_BODY_INITIAL_LENGTH> 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<unsigned short>(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<std::string, std::string> 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<std::size_t>(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<char, HTTP_BODY_INITIAL_LENGTH> 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<mg_connection, Disconnect> connection_;
std::string host_;
std::uint16_t port_;
SslConfig ssl_config_;
};
}}}} // namespace rti::apputils::http::client
#endif // HTTP_CLIENT_CONNECTION_HPP