diff options
author | Redouane Kachach <rkachach@redhat.com> | 2024-07-10 18:17:20 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-10 18:17:20 +0200 |
commit | 4194303966e28f468f75d5182b3d9e3fcdb50043 (patch) | |
tree | 29f019df62da50dc56a8fbb1f8acc06d5f3684bc /src | |
parent | Merge pull request #58343 from skritik098/rgw-module-period-commit-retry-fix (diff) | |
parent | exporter: Added https(TLSv13) support (diff) | |
download | ceph-4194303966e28f468f75d5182b3d9e3fcdb50043.tar.xz ceph-4194303966e28f468f75d5182b3d9e3fcdb50043.zip |
Merge pull request #58440 from jmolmo/tls_4_exporter
exporter: Added https(TLSv13) support
Reviewed-by: Ernesto Puerta <epuertat@redhat.com>
Reviewed-by: Avan Thakkar <athakkar@redhat.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/common/options/ceph-exporter.yaml.in | 14 | ||||
-rw-r--r-- | src/exporter/CMakeLists.txt | 6 | ||||
-rw-r--r-- | src/exporter/ceph_exporter.cc | 10 | ||||
-rw-r--r-- | src/exporter/http_server.cc | 169 | ||||
-rw-r--r-- | src/exporter/http_server.h | 5 | ||||
-rw-r--r-- | src/exporter/web_server.cc | 276 | ||||
-rw-r--r-- | src/exporter/web_server.h | 5 |
7 files changed, 307 insertions, 178 deletions
diff --git a/src/common/options/ceph-exporter.yaml.in b/src/common/options/ceph-exporter.yaml.in index 798a185e96b..c4b24ee43d4 100644 --- a/src/common/options/ceph-exporter.yaml.in +++ b/src/common/options/ceph-exporter.yaml.in @@ -25,6 +25,20 @@ options: default: 9926 services: - ceph-exporter +- name: exporter_cert_file + type: str + level: advanced + desc: Certificate file for TLS. + default: + services: + - ceph-exporter +- name: exporter_key_file + type: str + level: advanced + desc: Key certificate file for TLS. + default: + services: + - ceph-exporter - name: exporter_prio_limit type: int level: advanced diff --git a/src/exporter/CMakeLists.txt b/src/exporter/CMakeLists.txt index 0c0c03bf91d..0127cc53913 100644 --- a/src/exporter/CMakeLists.txt +++ b/src/exporter/CMakeLists.txt @@ -1,10 +1,12 @@ set(exporter_srcs ceph_exporter.cc DaemonMetricCollector.cc - http_server.cc + web_server.cc util.cc ) add_executable(ceph-exporter ${exporter_srcs}) target_link_libraries(ceph-exporter - global-static ceph-common) + global-static + ceph-common + OpenSSL::SSL) install(TARGETS ceph-exporter DESTINATION bin) diff --git a/src/exporter/ceph_exporter.cc b/src/exporter/ceph_exporter.cc index 70650ff87c6..2e2c16bb085 100644 --- a/src/exporter/ceph_exporter.cc +++ b/src/exporter/ceph_exporter.cc @@ -1,7 +1,7 @@ #include "common/ceph_argparse.h" #include "common/config.h" #include "exporter/DaemonMetricCollector.h" -#include "exporter/http_server.h" +#include "exporter/web_server.h" #include "global/global_init.h" #include "global/global_context.h" @@ -18,6 +18,8 @@ static void usage() { " --sock-dir: The path to ceph daemons socket files dir\n" " --addrs: Host ip address where exporter is deployed\n" " --port: Port to deploy exporter on. Default is 9926\n" + " --cert-file: Path to the certificate file to use https\n" + " --key-file: Path to the certificate key file to use https\n" " --prio-limit: Only perf counters greater than or equal to prio-limit are fetched. Default: 5\n" " --stats-period: Time to wait before sending requests again to exporter server (seconds). Default: 5s" << std::endl; @@ -48,6 +50,10 @@ int main(int argc, char **argv) { cct->_conf.set_val("exporter_addr", val); } else if (ceph_argparse_witharg(args, i, &val, "--port", (char *)NULL)) { cct->_conf.set_val("exporter_http_port", val); + } else if (ceph_argparse_witharg(args, i, &val, "--cert-file", (char *)NULL)) { + cct->_conf.set_val("exporter_cert_file", val); + } else if (ceph_argparse_witharg(args, i, &val, "--key-file", (char *)NULL)) { + cct->_conf.set_val("exporter_key_file", val); } else if (ceph_argparse_witharg(args, i, &val, "--prio-limit", (char *)NULL)) { cct->_conf.set_val("exporter_prio_limit", val); } else if (ceph_argparse_witharg(args, i, &val, "--stats-period", (char *)NULL)) { @@ -58,7 +64,7 @@ int main(int argc, char **argv) { } common_init_finish(g_ceph_context); - boost::thread server_thread(http_server_thread_entrypoint); + boost::thread server_thread(web_server_thread_entrypoint); DaemonMetricCollector &collector = collector_instance(); collector.main(); server_thread.join(); diff --git a/src/exporter/http_server.cc b/src/exporter/http_server.cc deleted file mode 100644 index 3eb48a2a1f0..00000000000 --- a/src/exporter/http_server.cc +++ /dev/null @@ -1,169 +0,0 @@ -#include "http_server.h" -#include "common/debug.h" -#include "common/hostname.h" -#include "global/global_init.h" -#include "global/global_context.h" -#include "exporter/DaemonMetricCollector.h" - -#include <boost/asio/ip/tcp.hpp> -#include <boost/beast/core.hpp> -#include <boost/beast/http.hpp> -#include <boost/beast/version.hpp> -#include <boost/thread/thread.hpp> -#include <chrono> -#include <cstdlib> -#include <ctime> -#include <iostream> -#include <map> -#include <memory> -#include <string> - -#define dout_context g_ceph_context -#define dout_subsys ceph_subsys_ceph_exporter - -namespace beast = boost::beast; // from <boost/beast.hpp> -namespace http = beast::http; // from <boost/beast/http.hpp> -namespace net = boost::asio; // from <boost/asio.hpp> -using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp> - -class http_connection : public std::enable_shared_from_this<http_connection> { -public: - http_connection(tcp::socket socket) : socket_(std::move(socket)) {} - - // Initiate the asynchronous operations associated with the connection. - void start() { - read_request(); - check_deadline(); - } - -private: - tcp::socket socket_; - beast::flat_buffer buffer_{8192}; - http::request<http::dynamic_body> request_; - http::response<http::string_body> response_; - - net::steady_timer deadline_{socket_.get_executor(), std::chrono::seconds(60)}; - - // Asynchronously receive a complete request message. - void read_request() { - auto self = shared_from_this(); - - http::async_read(socket_, buffer_, request_, - [self](beast::error_code ec, std::size_t bytes_transferred) { - boost::ignore_unused(bytes_transferred); - if (ec) { - dout(1) << "ERROR: " << ec.message() << dendl; - return; - } - else { - self->process_request(); - } - }); - } - - // Determine what needs to be done with the request message. - void process_request() { - response_.version(request_.version()); - response_.keep_alive(request_.keep_alive()); - - switch (request_.method()) { - case http::verb::get: - response_.result(http::status::ok); - create_response(); - break; - - default: - // We return responses indicating an error if - // we do not recognize the request method. - response_.result(http::status::method_not_allowed); - response_.set(http::field::content_type, "text/plain"); - std::string body("Invalid request-method '" + - std::string(request_.method_string()) + "'"); - response_.body() = body; - break; - } - - write_response(); - } - - // Construct a response message based on the program state. - void create_response() { - if (request_.target() == "/") { - response_.set(http::field::content_type, "text/html; charset=utf-8"); - std::string body("<html>\n" - "<head><title>Ceph Exporter</title></head>\n" - "<body>\n" - "<h1>Ceph Exporter</h1>\n" - "<p><a href='/metrics'>Metrics</a></p>" - "</body>\n" - "</html>\n"); - response_.body() = body; - } else if (request_.target() == "/metrics") { - response_.set(http::field::content_type, "text/plain; charset=utf-8"); - DaemonMetricCollector &collector = collector_instance(); - std::string metrics = collector.get_metrics(); - response_.body() = metrics; - } else { - response_.result(http::status::method_not_allowed); - response_.set(http::field::content_type, "text/plain"); - response_.body() = "File not found \n"; - } - } - - // Asynchronously transmit the response message. - void write_response() { - auto self = shared_from_this(); - - response_.prepare_payload(); - - http::async_write(socket_, response_, - [self](beast::error_code ec, std::size_t) { - self->socket_.shutdown(tcp::socket::shutdown_send, ec); - self->deadline_.cancel(); - if (ec) { - dout(1) << "ERROR: " << ec.message() << dendl; - return; - } - }); - } - - // Check whether we have spent enough time on this connection. - void check_deadline() { - auto self = shared_from_this(); - - deadline_.async_wait([self](beast::error_code ec) { - if (!ec) { - // Close socket to cancel any outstanding operation. - self->socket_.close(ec); - } - }); - } -}; - -// "Loop" forever accepting new connections. -void http_server(tcp::acceptor &acceptor, tcp::socket &socket) { - acceptor.async_accept(socket, [&](beast::error_code ec) { - if (!ec) - std::make_shared<http_connection>(std::move(socket))->start(); - http_server(acceptor, socket); - }); -} - -void http_server_thread_entrypoint() { - try { - std::string exporter_addr = g_conf().get_val<std::string>("exporter_addr"); - auto const address = net::ip::make_address(exporter_addr); - unsigned short port = g_conf().get_val<int64_t>("exporter_http_port"); - - net::io_context ioc{1}; - - tcp::acceptor acceptor{ioc, {address, port}}; - tcp::socket socket{ioc}; - http_server(acceptor, socket); - dout(1) << "Http server running on " << exporter_addr << ":" << port << dendl; - ioc.run(); - } catch (std::exception const &e) { - dout(1) << "Error: " << e.what() << dendl; - exit(EXIT_FAILURE); - } -} diff --git a/src/exporter/http_server.h b/src/exporter/http_server.h deleted file mode 100644 index 0d0502f57c8..00000000000 --- a/src/exporter/http_server.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include <string> - -void http_server_thread_entrypoint(); diff --git a/src/exporter/web_server.cc b/src/exporter/web_server.cc new file mode 100644 index 00000000000..96cc02b389f --- /dev/null +++ b/src/exporter/web_server.cc @@ -0,0 +1,276 @@ +#include "web_server.h" +#include "common/debug.h" +#include "common/hostname.h" +#include "global/global_init.h" +#include "global/global_context.h" +#include "exporter/DaemonMetricCollector.h" + +#include <boost/asio/ip/tcp.hpp> +#include <boost/asio/ssl.hpp> // SSL/TLS +#include <boost/beast/core.hpp> +#include <boost/beast/http.hpp> +#include <boost/beast/version.hpp> +#include <boost/thread/thread.hpp> +#include <chrono> +#include <cstdlib> +#include <ctime> +#include <iostream> +#include <map> +#include <memory> +#include <string> + +#define dout_context g_ceph_context +#define dout_subsys ceph_subsys_ceph_exporter + +namespace beast = boost::beast; // from <boost/beast.hpp> +namespace http = beast::http; // from <boost/beast/http.hpp> +namespace net = boost::asio; // from <boost/asio.hpp> +namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp> +using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp> + +// Base class for common functionality +class web_connection { +public: + virtual ~web_connection() = default; + virtual void start() = 0; // Pure virtual function to start the connection + +protected: + beast::flat_buffer buffer_{8192}; + http::request<http::dynamic_body> request_; + http::response<http::string_body> response_; + net::steady_timer deadline_; + + web_connection(net::any_io_executor executor, std::chrono::seconds timeout) + : deadline_(executor, timeout) {} + + // Common request processing logic + void process_request() { + response_.version(request_.version()); + response_.keep_alive(request_.keep_alive()); + + switch (request_.method()) { + case http::verb::get: + response_.result(http::status::ok); + create_response(); + break; + + default: + response_.result(http::status::method_not_allowed); + response_.set(http::field::content_type, "text/plain"); + std::string body("Invalid request-method '" + std::string(request_.method_string()) + "'\n"); + response_.body() = body; + break; + } + write_response(); + } + + // Construct a response message based on the request target + void create_response() { + if (request_.target() == "/") { + response_.result(http::status::moved_permanently); + response_.set(http::field::location, "/metrics"); + } else if (request_.target() == "/metrics") { + response_.set(http::field::content_type, "text/plain; charset=utf-8"); + DaemonMetricCollector &collector = collector_instance(); + std::string metrics = collector.get_metrics(); + response_.body() = metrics; + } else { + response_.result(http::status::method_not_allowed); + response_.set(http::field::content_type, "text/plain"); + response_.body() = "File not found \n"; + } + } + + // Asynchronously transmit the response message + virtual void write_response() = 0; + + // Check whether we have spent enough time on this connection + void check_deadline(std::shared_ptr<web_connection> self) { + deadline_.async_wait([self](beast::error_code ec) { + if (!ec) { + self->close_connection(ec); + } + }); + } + + // Bad requests error mgmt (http req->https srv and https req ->http srv) + void handle_bad_request(beast::error_code ec) { + response_.version(request_.version()); + response_.keep_alive(request_.keep_alive()); + response_.result(http::status::method_not_allowed); + response_.set(http::field::content_type, "text/plain"); + std::string body = "Ceph exporter.\nRequest Error: " + ec.message(); + response_.body() = body; + + write_response(); + } + + virtual void close_connection(beast::error_code& ec) = 0; +}; + +// Derived class for HTTP connections +class http_connection : public web_connection, public std::enable_shared_from_this<http_connection> { +public: + explicit http_connection(tcp::socket socket) + : web_connection(socket.get_executor(), std::chrono::seconds(60)), socket_(std::move(socket)) {} + + void start() override { + read_request(shared_from_this()); + check_deadline(shared_from_this()); + } + +private: + tcp::socket socket_; + + void read_request(std::shared_ptr<http_connection> self) { + http::async_read(socket_, buffer_, request_, + [self](beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) { + dout(1) << "ERROR: " << ec.message() << dendl; + self->handle_bad_request(ec); + return; + } + self->process_request(); + }); + } + + void write_response() override { + auto self = shared_from_this(); + response_.prepare_payload(); + http::async_write(socket_, response_, + [self](beast::error_code ec, std::size_t) { + self->socket_.shutdown(tcp::socket::shutdown_send, ec); + self->deadline_.cancel(); + if (ec) { + dout(1) << "ERROR: " << ec.message() << dendl; + return; + } + }); + } + + void close_connection(beast::error_code& ec) override { + socket_.close(ec); + } +}; + +// Derived class for HTTPS connections +class https_connection : public web_connection, public std::enable_shared_from_this<https_connection> { +public: + explicit https_connection(ssl::stream<tcp::socket> socket) + : web_connection(socket.get_executor(), std::chrono::seconds(60)), socket_(std::move(socket)) {} + + void start() override { + auto self = shared_from_this(); + socket_.async_handshake(ssl::stream_base::server, + [self](beast::error_code ec) { + if (!ec) { + self->read_request(self); + } else { + dout(1) << "ERROR: SSL Handshake failed: " << ec.message() << dendl; + self->handle_bad_request(ec); + } + }); + check_deadline(self); + } + +private: + ssl::stream<tcp::socket> socket_; + + void read_request(std::shared_ptr<https_connection> self) { + http::async_read(socket_, buffer_, request_, + [self](beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) { + dout(1) << "ERROR: " << ec.message() << dendl; + return; + } + self->process_request(); + }); + } + + void write_response() override { + auto self = shared_from_this(); + response_.prepare_payload(); + http::async_write(socket_, response_, + [self](beast::error_code ec, std::size_t) { + self->socket_.async_shutdown([self](beast::error_code ec) { + self->deadline_.cancel(); + if (ec) { + dout(1) << "ERROR: " << ec.message() << dendl; + } + }); + }); + } + + void close_connection(beast::error_code& ec) override { + socket_.lowest_layer().close(ec); + } + +}; + +void http_server(tcp::acceptor &acceptor, tcp::socket &socket) { + acceptor.async_accept(socket, [&](beast::error_code ec) { + if (!ec) { + std::make_shared<http_connection>(std::move(socket))->start(); + } + http_server(acceptor, socket); + }); +} + +void https_server(tcp::acceptor &acceptor, ssl::context &ssl_ctx) { + acceptor.async_accept([&](beast::error_code ec, tcp::socket socket) { + if (!ec) { + std::make_shared<https_connection>(ssl::stream<tcp::socket>(std::move(socket), ssl_ctx))->start(); + } + https_server(acceptor, ssl_ctx); + }); +} + +void run_http_server(const std::string& exporter_addr, short unsigned int port) { + net::io_context ioc{1}; + tcp::acceptor acceptor{ioc, {net::ip::make_address(exporter_addr), port}}; + tcp::socket socket{ioc}; + + http_server(acceptor, socket); + + dout(1) << "HTTP server running on " << exporter_addr << ":" << port << dendl; + ioc.run(); +} + +void run_https_server(const std::string& exporter_addr, short unsigned int port, const std::string& cert_file, const std::string& key_file) { + net::io_context ioc{1}; + ssl::context ssl_ctx(ssl::context::tlsv13); + + ssl_ctx.use_certificate_chain_file(cert_file); + ssl_ctx.use_private_key_file(key_file, ssl::context::pem); + + tcp::acceptor acceptor{ioc, {net::ip::make_address(exporter_addr), port}}; + https_server(acceptor, ssl_ctx); + + dout(1) << "HTTPS server running on " << exporter_addr << ":" << port << dendl; + ioc.run(); +} + +void web_server_thread_entrypoint() { + try { + std::string exporter_addr = g_conf().get_val<std::string>("exporter_addr"); + short unsigned int port = g_conf().get_val<int64_t>("exporter_http_port"); + std::string cert_file = g_conf().get_val<std::string>("exporter_cert_file"); + std::string key_file = g_conf().get_val<std::string>("exporter_key_file"); + + if (cert_file.empty() && key_file.empty()) { + run_http_server(exporter_addr, port); + } else { + try { + run_https_server(exporter_addr, port, cert_file, key_file); + } catch (const std::exception &e) { + dout(1) << "Failed to start HTTPS server: " << e.what() << dendl; + exit(EXIT_FAILURE); + } + } + } catch (std::exception const &e) { + dout(1) << "Error: " << e.what() << dendl; + exit(EXIT_FAILURE); + } +} diff --git a/src/exporter/web_server.h b/src/exporter/web_server.h new file mode 100644 index 00000000000..c3339a8d43a --- /dev/null +++ b/src/exporter/web_server.h @@ -0,0 +1,5 @@ +#pragma once + +#include <string> + +void web_server_thread_entrypoint(); |