]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
librbd/migration: basic http/https client implementation
authorJason Dillaman <dillaman@redhat.com>
Mon, 2 Nov 2020 23:14:10 +0000 (18:14 -0500)
committerJason Dillaman <dillaman@redhat.com>
Mon, 16 Nov 2020 23:16:24 +0000 (18:16 -0500)
The initial version only handles connecting, SSL handshaking, and
disconnecting. The next commit will add support for requests.

Signed-off-by: Jason Dillaman <dillaman@redhat.com>
src/librbd/CMakeLists.txt
src/librbd/migration/HttpClient.cc [new file with mode: 0644]
src/librbd/migration/HttpClient.h [new file with mode: 0644]
src/test/librbd/CMakeLists.txt
src/test/librbd/migration/test_mock_HttpClient.cc [new file with mode: 0644]
src/tools/rbd_mirror/CMakeLists.txt

index 940a365134d99979ed696efbc16726177a8c24ff..17fbad4c739a51e16e5e9990215261215efff468 100644 (file)
@@ -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 (file)
index 0000000..d3f4338
--- /dev/null
@@ -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 <boost/asio/buffer.hpp>
+#include <boost/asio/post.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/read.hpp>
+#include <boost/asio/ssl.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/ssl/ssl_stream.hpp>
+
+namespace librbd {
+namespace migration {
+
+template <typename I>
+struct HttpClient<I>::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 <typename I>
+template <typename D>
+class HttpClient<I>::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<D&>(*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 <typename I>
+class HttpClient<I>::PlainHttpSession : public HttpSession<PlainHttpSession> {
+public:
+  PlainHttpSession(HttpClient* http_client)
+    : HttpSession<PlainHttpSession>(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 <typename I>
+class HttpClient<I>::SslHttpSession : public HttpSession<SslHttpSession> {
+public:
+  SslHttpSession(HttpClient* http_client)
+    : HttpSession<SslHttpSession>(http_client),
+      m_stream(http_client->m_strand, http_client->m_ssl_context) {
+  }
+  ~SslHttpSession() override {
+    this->close();
+  }
+
+  inline boost::beast::ssl_stream<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;
+
+    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<boost::beast::tcp_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 <typename I>
+HttpClient<I>::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 <typename I>
+void HttpClient<I>::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 <typename I>
+void HttpClient<I>::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 <typename I>
+void HttpClient<I>::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<PlainHttpSession>(this);
+    break;
+  case URL_SCHEME_HTTPS:
+    m_http_session = std::make_unique<SslHttpSession>(this);
+    break;
+  default:
+    ceph_assert(false);
+    break;
+  }
+
+  m_http_session->init(on_finish);
+}
+
+} // namespace migration
+} // namespace librbd
+
+template class librbd::migration::HttpClient<librbd::ImageCtx>;
diff --git a/src/librbd/migration/HttpClient.h b/src/librbd/migration/HttpClient.h
new file mode 100644 (file)
index 0000000..bbfba44
--- /dev/null
@@ -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 <boost/asio/io_context_strand.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <memory>
+#include <string>
+#include <utility>
+
+struct Context;
+
+namespace librbd {
+
+struct AsioEngine;
+struct ImageCtx;
+
+namespace migration {
+
+template <typename ImageCtxT>
+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 <typename D> struct HttpSession;
+  struct PlainHttpSession;
+  struct SslHttpSession;
+
+  CephContext* m_cct;
+  std::shared_ptr<AsioEngine> 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<HttpSessionInterface> m_http_session;
+
+  void create_http_session(Context* on_finish);
+};
+
+} // namespace migration
+} // namespace librbd
+
+extern template class librbd::migration::HttpClient<librbd::ImageCtx>;
+
+#endif // CEPH_LIBRBD_MIGRATION_HTTP_CLIENT_H
index 5571712e23ad4946fab95955f573a7b732508637..8ee29357d6b157495eb98390a314b88591673acb 100644 (file)
@@ -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 (file)
index 0000000..25a6de5
--- /dev/null
@@ -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 <boost/asio/ip/tcp.hpp>
+
+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<MockTestImageCtx> 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 <typename Stream>
+  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 <typename Stream>
+  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<boost::asio::ip::tcp::acceptor> 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<boost::beast::tcp_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<boost::beast::tcp_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
index 5a89b6c3c9af0e6ec28b29f50da249147d511d3e..f260d978632a456afbd3986e74c9823c13558541 100644 (file)
@@ -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)