From: Jason Dillaman Date: Mon, 2 Nov 2020 23:14:10 +0000 (-0500) Subject: librbd/migration: basic http/https client implementation X-Git-Tag: v16.1.0~527^2~6 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=acd3da80ce107a03f6a8de202bc71747c5e6b313;p=ceph.git librbd/migration: basic http/https client implementation The initial version only handles connecting, SSL handshaking, and disconnecting. The next commit will add support for requests. Signed-off-by: Jason Dillaman --- diff --git a/src/librbd/CMakeLists.txt b/src/librbd/CMakeLists.txt index 940a365134d9..17fbad4c739a 100644 --- a/src/librbd/CMakeLists.txt +++ b/src/librbd/CMakeLists.txt @@ -126,6 +126,7 @@ set(librbd_internal_srcs managed_lock/ReleaseRequest.cc managed_lock/Utils.cc migration/FileStream.cc + migration/HttpClient.cc migration/ImageDispatch.cc migration/NativeFormat.cc migration/OpenSourceImageRequest.cc @@ -235,7 +236,8 @@ if(WITH_EVENTTRACE) add_dependencies(rbd_internal eventtrace_tp) endif() target_link_libraries(rbd_internal PRIVATE - osdc rbd_types) + osdc rbd_types + OpenSSL::SSL) target_include_directories(rbd_internal PRIVATE ${OPENSSL_INCLUDE_DIR}) if(WITH_RBD_RWL OR WITH_RBD_SSD_CACHE) diff --git a/src/librbd/migration/HttpClient.cc b/src/librbd/migration/HttpClient.cc new file mode 100644 index 000000000000..d3f4338a2b9d --- /dev/null +++ b/src/librbd/migration/HttpClient.cc @@ -0,0 +1,395 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include "librbd/migration/HttpClient.h" +#include "common/dout.h" +#include "common/errno.h" +#include "librbd/AsioEngine.h" +#include "librbd/ImageCtx.h" +#include "librbd/asio/Utils.h" +#include "librbd/migration/Utils.h" +#include +#include +#include +#include +#include +#include +#include + +namespace librbd { +namespace migration { + +template +struct HttpClient::HttpSessionInterface { + virtual ~HttpSessionInterface() {} + + virtual void init(Context* on_finish) = 0; + virtual void shut_down(Context* on_finish) = 0; +}; + +#define dout_subsys ceph_subsys_rbd +#undef dout_prefix +#define dout_prefix *_dout << "librbd::migration::HttpClient::" \ + << "HttpSession " << this << " " << __func__ \ + << ": " + +template +template +class HttpClient::HttpSession : public HttpSessionInterface { +public: + void init(Context* on_finish) override { + resolve_host(on_finish); + } + + void shut_down(Context* on_finish) override { + disconnect(new LambdaContext([this, on_finish](int r) { + handle_shut_down(r, on_finish); })); + } + +protected: + HttpClient* m_http_client; + + HttpSession(HttpClient* http_client) + : m_http_client(http_client), m_resolver(http_client->m_strand) { + } + + virtual void connect(boost::asio::ip::tcp::resolver::results_type results, + Context* on_finish) = 0; + virtual void disconnect(Context* on_finish) = 0; + + void close() { + boost::system::error_code ec; + boost::beast::get_lowest_layer(derived().stream()).socket().close(ec); + } + +private: + boost::asio::ip::tcp::resolver m_resolver; + + D& derived() { + return static_cast(*this); + } + + void resolve_host(Context* on_finish) { + auto cct = m_http_client->m_cct; + ldout(cct, 15) << dendl; + + m_resolver.async_resolve( + m_http_client->m_url_spec.host, m_http_client->m_url_spec.port, + asio::util::get_callback_adapter( + [this, on_finish](int r, auto results) { + handle_resolve_host(r, results, on_finish); })); + } + + void handle_resolve_host( + int r, boost::asio::ip::tcp::resolver::results_type results, + Context* on_finish) { + auto cct = m_http_client->m_cct; + ldout(cct, 15) << "r=" << r << dendl; + + if (r < 0) { + if (r == -boost::asio::error::host_not_found) { + r = -ENOENT; + } + + lderr(cct) << "failed to resolve host '" + << m_http_client->m_url_spec.host << "': " + << cpp_strerror(r) << dendl; + on_finish->complete(r); + return; + } + + connect(results, new LambdaContext([this, on_finish](int r) { + handle_connect(r, on_finish); })); + } + + void handle_connect(int r, Context* on_finish) { + auto cct = m_http_client->m_cct; + ldout(cct, 15) << "r=" << r << dendl; + + if (r < 0) { + lderr(cct) << "failed to connect to host '" + << m_http_client->m_url_spec.host << "': " + << cpp_strerror(r) << dendl; + on_finish->complete(r); + return; + } + + on_finish->complete(0); + } + + void handle_shut_down(int r, Context* on_finish) { + auto cct = m_http_client->m_cct; + ldout(cct, 15) << "r=" << r << dendl; + + if (r < 0) { + lderr(cct) << "failed to close stream: '" << cpp_strerror(r) << dendl; + on_finish->complete(r); + return; + } + + on_finish->complete(0); + } +}; + +#undef dout_prefix +#define dout_prefix *_dout << "librbd::migration::HttpClient::" \ + << "PlainHttpSession " << this << " " << __func__ \ + << ": " + +template +class HttpClient::PlainHttpSession : public HttpSession { +public: + PlainHttpSession(HttpClient* http_client) + : HttpSession(http_client), + m_stream(http_client->m_strand) { + } + ~PlainHttpSession() override { + this->close(); + } + + + inline boost::beast::tcp_stream& + stream() { + return m_stream; + } + +protected: + void connect(boost::asio::ip::tcp::resolver::results_type results, + Context* on_finish) override { + auto http_client = this->m_http_client; + auto cct = http_client->m_cct; + ldout(cct, 15) << dendl; + + m_stream.async_connect( + results, + asio::util::get_callback_adapter( + [on_finish](int r, auto endpoint) { on_finish->complete(r); })); + } + + void disconnect(Context* on_finish) override { + auto http_client = this->m_http_client; + auto cct = http_client->m_cct; + ldout(cct, 15) << dendl; + + boost::system::error_code ec; + m_stream.socket().shutdown( + boost::asio::ip::tcp::socket::shutdown_both, ec); + + on_finish->complete(-ec.value()); + } + +private: + boost::beast::tcp_stream m_stream; + +}; + +#undef dout_prefix +#define dout_prefix *_dout << "librbd::migration::HttpClient::" \ + << "SslHttpSession " << this << " " << __func__ \ + << ": " + +template +class HttpClient::SslHttpSession : public HttpSession { +public: + SslHttpSession(HttpClient* http_client) + : HttpSession(http_client), + m_stream(http_client->m_strand, http_client->m_ssl_context) { + } + ~SslHttpSession() override { + this->close(); + } + + inline boost::beast::ssl_stream& + stream() { + return m_stream; + } + +protected: + void connect(boost::asio::ip::tcp::resolver::results_type results, + Context* on_finish) override { + auto http_client = this->m_http_client; + auto cct = http_client->m_cct; + ldout(cct, 15) << dendl; + + boost::beast::get_lowest_layer(m_stream).async_connect( + results, + asio::util::get_callback_adapter( + [this, on_finish](int r, auto endpoint) { + handle_connect(r, on_finish); })); + } + + void disconnect(Context* on_finish) override { + auto http_client = this->m_http_client; + auto cct = http_client->m_cct; + ldout(cct, 15) << dendl; + + if (m_ssl_enabled) { + m_stream.async_shutdown( + asio::util::get_callback_adapter([this, on_finish](int r) { + shutdown(r, on_finish); })); + } else { + shutdown(0, on_finish); + } + } + +private: + boost::beast::ssl_stream m_stream; + bool m_ssl_enabled = false; + + void handle_connect(int r, Context* on_finish) { + auto http_client = this->m_http_client; + auto cct = http_client->m_cct; + ldout(cct, 15) << dendl; + + if (r < 0) { + lderr(cct) << "failed to connect to host '" + << http_client->m_url_spec.host << "': " + << cpp_strerror(r) << dendl; + on_finish->complete(r); + return; + } + + handshake(on_finish); + } + + void handshake(Context* on_finish) { + auto http_client = this->m_http_client; + auto cct = http_client->m_cct; + ldout(cct, 15) << dendl; + + auto& host = http_client->m_url_spec.host; + m_stream.set_verify_mode( + boost::asio::ssl::verify_peer | + boost::asio::ssl::verify_fail_if_no_peer_cert); + m_stream.set_verify_callback( + [host, next=boost::asio::ssl::host_name_verification(host), + ignore_self_signed=http_client->m_ignore_self_signed_cert] + (bool preverified, boost::asio::ssl::verify_context& ctx) { + if (!preverified && ignore_self_signed) { + auto ec = X509_STORE_CTX_get_error(ctx.native_handle()); + switch (ec) { + case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: + case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN: + // ignore self-signed cert issues + preverified = true; + break; + default: + break; + } + } + return next(preverified, ctx); + }); + + // Set SNI Hostname (many hosts need this to handshake successfully) + if(!SSL_set_tlsext_host_name(m_stream.native_handle(), + http_client->m_url_spec.host.c_str())) { + int r = -::ERR_get_error(); + lderr(cct) << "failed to initialize SNI hostname: " << cpp_strerror(r) + << dendl; + on_finish->complete(r); + return; + } + + // Perform the SSL/TLS handshake + m_stream.async_handshake( + boost::asio::ssl::stream_base::client, + asio::util::get_callback_adapter( + [this, on_finish](int r) { handle_handshake(r, on_finish); })); + } + + void handle_handshake(int r, Context* on_finish) { + auto http_client = this->m_http_client; + auto cct = http_client->m_cct; + ldout(cct, 15) << "r=" << r << dendl; + + if (r < 0) { + lderr(cct) << "failed to complete handshake: " << cpp_strerror(r) + << dendl; + disconnect(new LambdaContext([r, on_finish](int) { + on_finish->complete(r); })); + return; + } + + m_ssl_enabled = true; + on_finish->complete(0); + } + + void shutdown(int r, Context* on_finish) { + auto http_client = this->m_http_client; + auto cct = http_client->m_cct; + ldout(cct, 15) << "r=" << r << dendl; + + boost::system::error_code ec; + boost::beast::get_lowest_layer(m_stream).socket().shutdown( + boost::asio::ip::tcp::socket::shutdown_both, ec); + + on_finish->complete(-ec.value()); + } +}; + +#undef dout_prefix +#define dout_prefix *_dout << "librbd::migration::HttpClient: " << this \ + << " " << __func__ << ": " + +template +HttpClient::HttpClient(I* image_ctx, const std::string& url) + : m_cct(image_ctx->cct), m_asio_engine(image_ctx->asio_engine), m_url(url), + m_strand(*m_asio_engine), + m_ssl_context(boost::asio::ssl::context::sslv23_client) { + m_ssl_context.set_default_verify_paths(); +} + +template +void HttpClient::open(Context* on_finish) { + ldout(m_cct, 10) << "url=" << m_url << dendl; + + int r = util::parse_url(m_cct, m_url, &m_url_spec); + if (r < 0) { + lderr(m_cct) << "failed to parse url '" << m_url << "': " << cpp_strerror(r) + << dendl; + on_finish->complete(-EINVAL); + return; + } + + // initial bootstrap connection -- later IOs might rebuild the session + create_http_session(on_finish); +} + +template +void HttpClient::close(Context* on_finish) { + // execute within the strand to ensure all IO completes + boost::asio::post( + m_strand, [this, on_finish]() { + if (m_http_session == nullptr) { + on_finish->complete(0); + return; + } + + m_http_session->shut_down(on_finish); + }); +} + +template +void HttpClient::create_http_session(Context* on_finish) { + ldout(m_cct, 15) << dendl; + + ceph_assert(m_http_session == nullptr); + switch (m_url_spec.scheme) { + case URL_SCHEME_HTTP: + m_http_session = std::make_unique(this); + break; + case URL_SCHEME_HTTPS: + m_http_session = std::make_unique(this); + break; + default: + ceph_assert(false); + break; + } + + m_http_session->init(on_finish); +} + +} // namespace migration +} // namespace librbd + +template class librbd::migration::HttpClient; diff --git a/src/librbd/migration/HttpClient.h b/src/librbd/migration/HttpClient.h new file mode 100644 index 000000000000..bbfba449c318 --- /dev/null +++ b/src/librbd/migration/HttpClient.h @@ -0,0 +1,71 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#ifndef CEPH_LIBRBD_MIGRATION_HTTP_CLIENT_H +#define CEPH_LIBRBD_MIGRATION_HTTP_CLIENT_H + +#include "include/common_fwd.h" +#include "include/int_types.h" +#include "librbd/migration/Types.h" +#include +#include +#include +#include +#include +#include + +struct Context; + +namespace librbd { + +struct AsioEngine; +struct ImageCtx; + +namespace migration { + +template +class HttpClient { +public: + static HttpClient* create(ImageCtxT* image_ctx, const std::string& url) { + return new HttpClient(image_ctx, url); + } + + HttpClient(ImageCtxT* image_ctx, const std::string& url); + HttpClient(const HttpClient&) = delete; + HttpClient& operator=(const HttpClient&) = delete; + + void open(Context* on_finish); + void close(Context* on_finish); + + void set_ignore_self_signed_cert(bool ignore) { + m_ignore_self_signed_cert = ignore; + } + +private: + struct HttpSessionInterface; + template struct HttpSession; + struct PlainHttpSession; + struct SslHttpSession; + + CephContext* m_cct; + std::shared_ptr m_asio_engine; + std::string m_url; + + UrlSpec m_url_spec; + + bool m_ignore_self_signed_cert = false; + + boost::asio::io_context::strand m_strand; + + boost::asio::ssl::context m_ssl_context; + std::unique_ptr m_http_session; + + void create_http_session(Context* on_finish); +}; + +} // namespace migration +} // namespace librbd + +extern template class librbd::migration::HttpClient; + +#endif // CEPH_LIBRBD_MIGRATION_HTTP_CLIENT_H diff --git a/src/test/librbd/CMakeLists.txt b/src/test/librbd/CMakeLists.txt index 5571712e23ad..8ee29357d6b1 100644 --- a/src/test/librbd/CMakeLists.txt +++ b/src/test/librbd/CMakeLists.txt @@ -90,6 +90,7 @@ set(unittest_librbd_srcs managed_lock/test_mock_ReacquireRequest.cc managed_lock/test_mock_ReleaseRequest.cc migration/test_mock_FileStream.cc + migration/test_mock_HttpClient.cc migration/test_mock_RawFormat.cc migration/test_mock_Utils.cc mirror/snapshot/test_mock_CreateNonPrimaryRequest.cc @@ -155,6 +156,7 @@ target_link_libraries(unittest_librbd osdc ceph-common global + OpenSSL::SSL ${UNITTEST_LIBS}) add_executable(ceph_test_librbd diff --git a/src/test/librbd/migration/test_mock_HttpClient.cc b/src/test/librbd/migration/test_mock_HttpClient.cc new file mode 100644 index 000000000000..25a6de519c6d --- /dev/null +++ b/src/test/librbd/migration/test_mock_HttpClient.cc @@ -0,0 +1,294 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include "test/librbd/test_mock_fixture.h" +#include "test/librbd/test_support.h" +#include "include/rbd_types.h" +#include "common/ceph_mutex.h" +#include "librbd/migration/HttpClient.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include + +namespace librbd { +namespace { + +struct MockTestImageCtx : public MockImageCtx { + MockTestImageCtx(ImageCtx &image_ctx) : MockImageCtx(image_ctx) { + } +}; + +} // anonymous namespace +} // namespace librbd + +#include "librbd/migration/HttpClient.cc" + +namespace librbd { +namespace migration { + +using ::testing::Invoke; + +class TestMockMigrationHttpClient : public TestMockFixture { +public: + typedef HttpClient MockHttpClient; + + void SetUp() override { + TestMockFixture::SetUp(); + + ASSERT_EQ(0, open_image(m_image_name, &m_image_ctx)); + + m_acceptor.emplace(*m_image_ctx->asio_engine, + boost::asio::ip::tcp::endpoint( + boost::asio::ip::tcp::v4(), 0)); + m_server_port = m_acceptor->local_endpoint().port(); + } + + void TearDown() override { + m_acceptor.reset(); + + TestMockFixture::TearDown(); + } + + std::string get_local_url(UrlScheme url_scheme) { + std::stringstream sstream; + switch (url_scheme) { + case URL_SCHEME_HTTP: + sstream << "http://127.0.0.1"; + break; + case URL_SCHEME_HTTPS: + sstream << "https://localhost"; + break; + default: + ceph_assert(false); + break; + } + + sstream << ":" << m_server_port << "/"; + return sstream.str(); + } + + void client_accept(boost::asio::ip::tcp::socket* socket, bool close, + Context* on_connect) { + m_acceptor->async_accept( + boost::asio::make_strand(m_image_ctx->asio_engine->get_executor()), + [socket, close, on_connect] + (auto ec, boost::asio::ip::tcp::socket in_socket) { + if (close) { + in_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both); + } else { + ASSERT_EQ(0, ec.value()); + *socket = std::move(in_socket); + } + on_connect->complete(0); + }); + } + + template + void client_ssl_handshake(Stream& stream, Context* on_handshake) { + stream.async_handshake( + boost::asio::ssl::stream_base::server, + [on_handshake](auto ec) { + on_handshake->complete(-ec.value()); + }); + } + + template + void client_ssl_shutdown(Stream& stream, Context* on_shutdown) { + stream.async_shutdown( + [on_shutdown](auto ec) { + on_shutdown->complete(-ec.value()); + }); + } + + void load_server_certificate(boost::asio::ssl::context& ctx) { + ctx.set_options( + boost::asio::ssl::context::default_workarounds | + boost::asio::ssl::context::no_sslv2 | + boost::asio::ssl::context::single_dh_use); + ctx.use_certificate_chain( + boost::asio::buffer(CERT.data(), CERT.size())); + ctx.use_private_key( + boost::asio::buffer(KEY.data(), KEY.size()), + boost::asio::ssl::context::file_format::pem); + ctx.use_tmp_dh( + boost::asio::buffer(DH.data(), DH.size())); + } + + // dummy self-signed cert for localhost + const std::string CERT = + "-----BEGIN CERTIFICATE-----\n" + "MIIDXzCCAkegAwIBAgIUYH6rAaq66LC6yJ3XK1WEMIfmY4cwDQYJKoZIhvcNAQEL\n" + "BQAwPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMQ8wDQYDVQQHDAZNY0xlYW4x\n" + "EjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMDExMDIyMTM5NTVaFw00ODAzMjAyMTM5\n" + "NTVaMD8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJWQTEPMA0GA1UEBwwGTWNMZWFu\n" + "MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n" + "AoIBAQCeRkyxjP0eNHxzj4/R+Bg/31p7kEjB5d/LYtrzBIYNe+3DN8gdReixEpR5\n" + "lgTLDsl8gfk2HRz4cnAiseqYL6GKtw/cFadzLyXTbW4iavmTWiGYw/8RJlKunbhA\n" + "hDjM6H99ysLf0NS6t14eK+bEJIW1PiTYRR1U5I4kSIjpCX7+nJVuwMEZ2XBpN3og\n" + "nHhv2hZYTdzEkQEyZHz4V/ApfD7rlja5ecd/vJfPJeA8nudnGCh3Uo6f8I9TObAj\n" + "8hJdfRiRBvnA4NnkrMrxW9UtVjScnw9Xia11FM/IGJIgMpLQ5dqBw930p6FxMYtn\n" + "tRD1AF9sT+YjoCaHv0hXZvBEUEF3AgMBAAGjUzBRMB0GA1UdDgQWBBTQoIiX3+p/\n" + "P4Xz2vwERz6pbjPGhzAfBgNVHSMEGDAWgBTQoIiX3+p/P4Xz2vwERz6pbjPGhzAP\n" + "BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCVKoYAw+D1qqWRDSh3\n" + "2KlKMnT6sySo7XmReGArj8FTKiZUprByj5CfAtaiDSdPOpcg3EazWbdasZbMmSQm\n" + "+jpe5WoKnxL9b12lwwUYHrLl6RlrDHVkIVlXLNbJFY5TpfjvZfHpwVAygF3fnbgW\n" + "PPuODUNAS5NDwST+t29jBZ/wwU0pyW0CS4K5d3XMGHBc13j2V/FyvmsZ5xfA4U9H\n" + "oEnmZ/Qm+FFK/nR40rTAZ37cuv4ysKFtwvatNgTfHGJwaBUkKFdDbcyxt9abCi6x\n" + "/K+ScoJtdIeVcfx8Fnc5PNtSpy8bHI3Zy4IEyw4kOqwwI1h37iBafZ2WdQkTxlAx\n" + "JIDj\n" + "-----END CERTIFICATE-----\n"; + const std::string KEY = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCeRkyxjP0eNHxz\n" + "j4/R+Bg/31p7kEjB5d/LYtrzBIYNe+3DN8gdReixEpR5lgTLDsl8gfk2HRz4cnAi\n" + "seqYL6GKtw/cFadzLyXTbW4iavmTWiGYw/8RJlKunbhAhDjM6H99ysLf0NS6t14e\n" + "K+bEJIW1PiTYRR1U5I4kSIjpCX7+nJVuwMEZ2XBpN3ognHhv2hZYTdzEkQEyZHz4\n" + "V/ApfD7rlja5ecd/vJfPJeA8nudnGCh3Uo6f8I9TObAj8hJdfRiRBvnA4NnkrMrx\n" + "W9UtVjScnw9Xia11FM/IGJIgMpLQ5dqBw930p6FxMYtntRD1AF9sT+YjoCaHv0hX\n" + "ZvBEUEF3AgMBAAECggEACCaYpoAbPOX5Dr5y6p47KXboIvrgNFQRPVke62rtOF6M\n" + "dQQ3YwKJpCzPxp8qKgbd63KKEfZX2peSHMdKzIGPcSRSRcQ7tlvUN9on1M/rgGIg\n" + "3swhI5H0qhdnOLNWdX73qdO6S2pmuiLdTvJ11N4IoLfNj/GnPAr1Ivs1ScL6bkQv\n" + "UybaNQ/g2lB0tO7vUeVe2W/AqsIb1eQlf2g+SH7xRj2bGQkr4cWTylqfiVoL/Xic\n" + "QVTCks3BWaZhYIhTFgvqVhXZpp52O9J+bxsWJItKQrrCBemxwp82xKbiW/KoI9L1\n" + "wSnKvxx7Q3RUN5EvXeOpTRR8QIpBoxP3TTeoj+EOMQKBgQDQb/VfLDlLgfYJpgRC\n" + "hKCLW90un9op3nA2n9Dmm9TTLYOmUyiv5ub8QDINEw/YK/NE2JsTSUk2msizqTLL\n" + "Z82BFbz9kPlDbJ5MgxG5zXeLvOLurAFmZk/z5JJO+65PKjf0QVLncSAJvMCeNFuC\n" + "2yZrEzbrItrjQsN6AedWdx6TTwKBgQDCZAsSI3lQgOh2q1GSxjuIzRAc7JnSGBvD\n" + "nG8+SkfKAy7BWe638772Dgx8KYO7TLI4zlm8c9Tr/nkZsGWmM5S2DMI69PWOQWNa\n" + "R6QzOFFwNg2JETH7ow+x8+9Q9d3WsPzROz3r5uDXgEk0glthaymVoPILFOiYpz3r\n" + "heUbd6mFWQKBgQCCJBVJGhyf54IOHij0u0heGrpr/QTDNY5MnNZa1hs4y2cydyOl\n" + "SH8aKp7ViPxQlYhriO6ySQS8YkJD4rXDSImIOmFo1Ja9oVjpHsD3iLFGf2YVbTHm\n" + "lKUA+8raI8x+wzZyfELeHMTLL534aWpltp0zJ6kXgQi38pyIVh3x36gogwKBgQCt\n" + "nba5k49VVFzLKEXqBjzD+QqMGtFjcH7TnZNJmgQ2K9OFgzIPf5atomyKNHXgQibn\n" + "T32cMAQaZqR4SjDvWSBX3FtZVtE+Ja57woKn8IPj6ZL7Oa1fpwpskIbM01s31cln\n" + "gjbSy9lC/+PiDw9YmeKBLkcfmKQJO021Xlf6yUxRuQKBgBWPODUO8oKjkuJXNI/w\n" + "El9hNWkd+/SZDfnt93dlVyXTtTF0M5M95tlOgqvLtWKSyB/BOnoZYWqR8luMl15d\n" + "bf75j5mB0lHMWtyQgvZSkFqe9Or7Zy7hfTShDlZ/w+OXK7PGesaE1F14irShXSji\n" + "yn5DZYAZ5pU52xreJeYvDngO\n" + "-----END PRIVATE KEY-----\n"; + const std::string DH = + "-----BEGIN DH PARAMETERS-----\n" + "MIIBCAKCAQEA4+DA1j0gDWS71okwHpnvA65NmmR4mf+B3H39g163zY5S+cnWS2LI\n" + "dvqnUDpw13naWtQ+Nu7I4rk1XoPaxOPSTu1MTbtYOxxU9M1ceBu4kQjDeHwasPVM\n" + "zyEs1XXX3tsbPUxAuayX+AgW6QQAQUEjKDnv3FzVnQTFjwI49LqjnrSjbgQcoMaH\n" + "EdGGUc6t1/We2vtsJZx0/dbaMkzFYO8dAbEYHL4sPKQb2mLpCPJZC3vwzpFkHFCd\n" + "QSnLW2qRhy+66Mf8shdr6uvpoMcnKMOAvjKdXl9PBeJM9eJPz2lC4tnTiM3DqNzK\n" + "Hn8+Pu3KkSIFL/5uBVu1fZSq+lFIEI23wwIBAg==\n" + "-----END DH PARAMETERS-----\n"; + + librbd::ImageCtx *m_image_ctx; + + std::optional m_acceptor; + uint64_t m_server_port = 0; +}; + +TEST_F(TestMockMigrationHttpClient, OpenCloseHttp) { + boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); + C_SaferCond on_connect_ctx; + client_accept(&socket, false, &on_connect_ctx); + + MockTestImageCtx mock_test_image_ctx(*m_image_ctx); + MockHttpClient http_client(&mock_test_image_ctx, + get_local_url(URL_SCHEME_HTTP)); + + C_SaferCond ctx1; + http_client.open(&ctx1); + ASSERT_EQ(0, on_connect_ctx.wait()); + ASSERT_EQ(0, ctx1.wait()); + + C_SaferCond ctx2; + http_client.close(&ctx2); + ASSERT_EQ(0, ctx2.wait()); +} + +TEST_F(TestMockMigrationHttpClient, OpenCloseHttps) { + boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); + C_SaferCond on_connect_ctx; + client_accept(&socket, false, &on_connect_ctx); + + MockTestImageCtx mock_test_image_ctx(*m_image_ctx); + MockHttpClient http_client(&mock_test_image_ctx, + get_local_url(URL_SCHEME_HTTPS)); + http_client.set_ignore_self_signed_cert(true); + + C_SaferCond ctx1; + http_client.open(&ctx1); + ASSERT_EQ(0, on_connect_ctx.wait()); + + boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tlsv12}; + load_server_certificate(ssl_context); + boost::beast::ssl_stream ssl_stream{ + std::move(socket), ssl_context}; + + C_SaferCond on_ssl_handshake_ctx; + client_ssl_handshake(ssl_stream, &on_ssl_handshake_ctx); + ASSERT_EQ(0, on_ssl_handshake_ctx.wait()); + + ASSERT_EQ(0, ctx1.wait()); + + C_SaferCond ctx2; + http_client.close(&ctx2); + + C_SaferCond on_ssl_shutdown_ctx; + client_ssl_shutdown(ssl_stream, &on_ssl_shutdown_ctx); + ASSERT_EQ(0, on_ssl_shutdown_ctx.wait()); + + ASSERT_EQ(0, ctx2.wait()); +} + +TEST_F(TestMockMigrationHttpClient, OpenHttpsHandshakeFail) { + boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); + C_SaferCond on_connect_ctx; + client_accept(&socket, false, &on_connect_ctx); + + MockTestImageCtx mock_test_image_ctx(*m_image_ctx); + MockHttpClient http_client(&mock_test_image_ctx, + get_local_url(URL_SCHEME_HTTPS)); + + C_SaferCond ctx1; + http_client.open(&ctx1); + ASSERT_EQ(0, on_connect_ctx.wait()); + + boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tlsv12}; + load_server_certificate(ssl_context); + boost::beast::ssl_stream ssl_stream{ + std::move(socket), ssl_context}; + + C_SaferCond on_ssl_handshake_ctx; + client_ssl_handshake(ssl_stream, &on_ssl_handshake_ctx); + ASSERT_NE(0, on_ssl_handshake_ctx.wait()); + ASSERT_NE(0, ctx1.wait()); +} + +TEST_F(TestMockMigrationHttpClient, OpenInvalidUrl) { + MockTestImageCtx mock_test_image_ctx(*m_image_ctx); + MockHttpClient http_client(&mock_test_image_ctx, "ftp://nope/"); + + C_SaferCond ctx; + http_client.open(&ctx); + ASSERT_EQ(-EINVAL, ctx.wait()); +} + +TEST_F(TestMockMigrationHttpClient, OpenResolveFail) { + MockTestImageCtx mock_test_image_ctx(*m_image_ctx); + MockHttpClient http_client(&mock_test_image_ctx, "http://invalid.ceph.com"); + + C_SaferCond ctx; + http_client.open(&ctx); + ASSERT_EQ(-ENOENT, ctx.wait()); +} + +TEST_F(TestMockMigrationHttpClient, OpenConnectFail) { + m_acceptor.reset(); + + MockTestImageCtx mock_test_image_ctx(*m_image_ctx); + MockHttpClient http_client(&mock_test_image_ctx, + get_local_url(URL_SCHEME_HTTP)); + + C_SaferCond ctx1; + http_client.open(&ctx1); + ASSERT_EQ(-ECONNREFUSED, ctx1.wait()); +} + +} // namespace migration +} // namespace librbd diff --git a/src/tools/rbd_mirror/CMakeLists.txt b/src/tools/rbd_mirror/CMakeLists.txt index 5a89b6c3c9af..f260d978632a 100644 --- a/src/tools/rbd_mirror/CMakeLists.txt +++ b/src/tools/rbd_mirror/CMakeLists.txt @@ -85,5 +85,6 @@ target_link_libraries(rbd-mirror cls_journal_client global heap_profiler - ${ALLOC_LIBS}) + ${ALLOC_LIBS} + OpenSSL::SSL) install(TARGETS rbd-mirror DESTINATION bin)