From 9fc12fbe0fae75cea09c734c12ae02a45eb0fb09 Mon Sep 17 00:00:00 2001 From: Francis Dupont Date: Fri, 19 Jul 2024 19:29:33 +0200 Subject: [PATCH] [#3490] Moved the HTTPS variant --- src/lib/http/tests/tls_server_unittests.cc | 242 +------------- src/lib/http/testutils/test_http_client.h | 346 ++++++++++++++++++++- 2 files changed, 336 insertions(+), 252 deletions(-) diff --git a/src/lib/http/tests/tls_server_unittests.cc b/src/lib/http/tests/tls_server_unittests.cc index 81f758d607..10b82641f9 100644 --- a/src/lib/http/tests/tls_server_unittests.cc +++ b/src/lib/http/tests/tls_server_unittests.cc @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -386,247 +387,6 @@ public: } }; - -/// @brief Entity which can connect to the HTTP server endpoint. -class TestHttpsClient : public boost::noncopyable { -public: - - /// @brief Constructor. - /// - /// This constructor creates new socket instance. It doesn't connect. Call - /// connect() to connect to the server. - /// - /// @param io_service IO service to be stopped on error. - /// @param tls_context TLS context. - TestHttpsClient(const IOServicePtr& io_service, TlsContextPtr tls_context) - : io_service_(io_service), stream_(io_service_->getInternalIOService(), - tls_context->getContext()), buf_(), response_() { - } - - /// @brief Destructor. - /// - /// Closes the underlying socket if it is open. - ~TestHttpsClient() { - close(); - } - - /// @brief Send HTTP request specified in textual format. - /// - /// @param request HTTP request in the textual format. - void startRequest(const std::string& request) { - tcp::endpoint endpoint(address::from_string(SERVER_ADDRESS), - SERVER_PORT); - stream_.lowest_layer().async_connect(endpoint, - [this, request](const boost::system::error_code& ec) { - if (ec) { - // One would expect that async_connect wouldn't return - // EINPROGRESS error code, but simply wait for the connection - // to get established before the handler is invoked. It turns out, - // however, that on some OSes the connect handler may receive this - // error code which doesn't necessarily indicate a problem. - // Making an attempt to write and read from this socket will - // typically succeed. So, we ignore this error. - if (ec.value() != boost::asio::error::in_progress) { - ADD_FAILURE() << "error occurred while connecting: " - << ec.message(); - io_service_->stop(); - return; - } - } - stream_.async_handshake(roleToImpl(TlsRole::CLIENT), - [this, request](const boost::system::error_code& ec) { - if (ec) { - ADD_FAILURE() << "error occurred during handshake: " - << ec.message(); - io_service_->stop(); - return; - } - sendRequest(request); - }); - }); - } - - /// @brief Send HTTP request. - /// - /// @param request HTTP request in the textual format. - void sendRequest(const std::string& request) { - sendPartialRequest(request); - } - - /// @brief Send part of the HTTP request. - /// - /// @param request part of the HTTP request to be sent. - void sendPartialRequest(std::string request) { - boost::asio::async_write(stream_, - boost::asio::buffer(request.data(), request.size()), - [this, request](const boost::system::error_code& ec, - std::size_t bytes_transferred) mutable { - if (ec) { - if (ec.value() == boost::asio::error::operation_aborted) { - return; - - } else if ((ec.value() == boost::asio::error::try_again) || - (ec.value() == boost::asio::error::would_block)) { - // If we should try again make sure there is no garbage in the - // bytes_transferred. - bytes_transferred = 0; - - } else { - ADD_FAILURE() << "error occurred while connecting: " - << ec.message(); - io_service_->stop(); - return; - } - } - - // Remove the part of the request which has been sent. - if (bytes_transferred > 0 && (request.size() <= bytes_transferred)) { - request.erase(0, bytes_transferred); - } - - // Continue sending request data if there are still some data to be - // sent. - if (!request.empty()) { - sendPartialRequest(request); - - } else { - // Request has been sent. Start receiving response. - response_.clear(); - receivePartialResponse(); - } - }); - } - - /// @brief Receive response from the server. - void receivePartialResponse() { - stream_.async_read_some(boost::asio::buffer(buf_.data(), buf_.size()), - [this](const boost::system::error_code& ec, - std::size_t bytes_transferred) { - if (ec) { - // IO service stopped so simply return. - if (ec.value() == boost::asio::error::operation_aborted) { - return; - - } else if ((ec.value() == boost::asio::error::try_again) || - (ec.value() == boost::asio::error::would_block)) { - // If we should try again, make sure that there is no garbage - // in the bytes_transferred. - bytes_transferred = 0; - - } else { - // Error occurred, bail... - ADD_FAILURE() << "error occurred while receiving HTTP" - " response from the server: " << ec.message(); - io_service_->stop(); - } - } - - if (bytes_transferred > 0) { - response_.insert(response_.end(), buf_.data(), - buf_.data() + bytes_transferred); - } - - // Two consecutive new lines end the part of the response we're - // expecting. - if (response_.find("\r\n\r\n", 0) != std::string::npos) { - io_service_->stop(); - - } else { - receivePartialResponse(); - } - - }); - } - - /// @brief Checks if the TCP connection is still open. - /// - /// Tests the TCP connection by trying to read from the socket. - /// Unfortunately expected failure depends on a race between the read - /// and the other side close so to check if the connection is closed - /// please use @c isConnectionClosed instead. - /// - /// @return true if the TCP connection is open. - bool isConnectionAlive() { - // Remember the current non blocking setting. - const bool non_blocking_orig = stream_.lowest_layer().non_blocking(); - // Set the socket to non blocking mode. We're going to test if the socket - // returns would_block status on the attempt to read from it. - stream_.lowest_layer().non_blocking(true); - - // We need to provide a buffer for a call to read. - char data[2]; - boost::system::error_code ec; - boost::asio::read(stream_, boost::asio::buffer(data, sizeof(data)), ec); - - // Revert the original non_blocking flag on the socket. - stream_.lowest_layer().non_blocking(non_blocking_orig); - - // If the connection is alive we'd typically get would_block status code. - // If there are any data that haven't been read we may also get success - // status. We're guessing that try_again may also be returned by some - // implementations in some situations. Any other error code indicates a - // problem with the connection so we assume that the connection has been - // closed. - return (!ec || (ec.value() == boost::asio::error::try_again) || - (ec.value() == boost::asio::error::would_block)); - } - - /// @brief Checks if the TCP connection is already closed. - /// - /// Tests the TCP connection by trying to read from the socket. - /// The read can block so this must be used to check if a connection - /// is alive so to check if the connection is alive please always - /// use @c isConnectionAlive. - /// - /// @return true if the TCP connection is closed. - bool isConnectionClosed() { - // Remember the current non blocking setting. - const bool non_blocking_orig = stream_.lowest_layer().non_blocking(); - // Set the socket to blocking mode. We're going to test if the socket - // returns eof status on the attempt to read from it. - stream_.lowest_layer().non_blocking(false); - - // We need to provide a buffer for a call to read. - char data[2]; - boost::system::error_code ec; - boost::asio::read(stream_, boost::asio::buffer(data, sizeof(data)), ec); - - // Revert the original non_blocking flag on the socket. - stream_.lowest_layer().non_blocking(non_blocking_orig); - - // If the connection is closed we'd typically get eof or - // stream_truncated status code. - return ((ec.value() == boost::asio::error::eof) || - (ec.value() == STREAM_TRUNCATED)); - } - - /// @brief Close connection. - void close() { - stream_.lowest_layer().close(); - } - - std::string getResponse() const { - return (response_); - } - -private: - - /// @brief Holds pointer to the IO service. - isc::asiolink::IOServicePtr io_service_; - - /// @brief A socket used for the connection. - TlsStreamImpl stream_; - - /// @brief Buffer into which response is written. - std::array buf_; - - /// @brief Response in the textual format. - std::string response_; -}; - -/// @brief Pointer to the TestHttpsClient. -typedef boost::shared_ptr TestHttpsClientPtr; - /// @brief Test fixture class for @ref HttpListener. class HttpsListenerTest : public ::testing::Test { public: diff --git a/src/lib/http/testutils/test_http_client.h b/src/lib/http/testutils/test_http_client.h index 64db858db3..eaf2d60e77 100644 --- a/src/lib/http/testutils/test_http_client.h +++ b/src/lib/http/testutils/test_http_client.h @@ -19,8 +19,68 @@ using namespace boost::asio::ip; using namespace isc::asiolink; +/// @brief Common base for test HTTP/HTTPS clients. +class BaseTestHttpClient : public boost::noncopyable { +public: + + /// @brief Destructor. + virtual ~BaseTestHttpClient() = default; + + /// @brief Send HTTP request specified in textual format. + /// + /// @param request HTTP request in the textual format. + virtual void startRequest(const std::string& request) = 0; + + /// @brief Send HTTP request. + /// + /// @param request HTTP request in the textual format. + virtual void sendRequest(const std::string& request) = 0; + + /// @brief Send part of the HTTP request. + /// + /// @param request part of the HTTP request to be sent. + virtual void sendPartialRequest(std::string request) = 0; + + /// @brief Receive response from the server. + virtual void receivePartialResponse() = 0; + + /// @brief Checks if the TCP connection is still open. + /// + /// Tests the TCP connection by trying to read from the socket. + /// Unfortunately expected failure depends on a race between the read + /// and the other side close so to check if the connection is closed + /// please use @c isConnectionClosed instead. + /// + /// @return true if the TCP connection is open. + virtual bool isConnectionAlive() = 0; + + /// @brief Checks if the TCP connection is already closed. + /// + /// Tests the TCP connection by trying to read from the socket. + /// The read can block so this must be used to check if a connection + /// is alive so to check if the connection is alive please always + /// use @c isConnectionAlive. + /// + /// @return true if the TCP connection is closed. + virtual bool isConnectionClosed() = 0; + + /// @brief Close connection. + virtual void close() = 0; + + /// @brief Returns the HTTP response string. + /// + /// @return string containing the response. + virtual std::string getResponse() const = 0; + + /// @brief Returns true if the receive completed without error. + /// + /// @return True if the receive completed successfully, false + /// otherwise. + virtual bool receiveDone() const = 0; +}; + /// @brief Entity which can connect to the HTTP server endpoint. -class TestHttpClient : public boost::noncopyable { +class TestHttpClient : public BaseTestHttpClient { public: /// @brief Constructor. @@ -42,14 +102,14 @@ public: /// @brief Destructor. /// /// Closes the underlying socket if it is open. - ~TestHttpClient() { + virtual ~TestHttpClient() { close(); } /// @brief Send HTTP request specified in textual format. /// /// @param request HTTP request in the textual format. - void startRequest(const std::string& request) { + virtual void startRequest(const std::string& request) { tcp::endpoint endpoint(address::from_string(server_address_), server_port_); socket_.async_connect(endpoint, [this, request](const boost::system::error_code& ec) { @@ -76,14 +136,14 @@ public: /// @brief Send HTTP request. /// /// @param request HTTP request in the textual format. - void sendRequest(const std::string& request) { + virtual void sendRequest(const std::string& request) { sendPartialRequest(request); } /// @brief Send part of the HTTP request. /// /// @param request part of the HTTP request to be sent. - void sendPartialRequest(std::string request) { + virtual void sendPartialRequest(std::string request) { socket_.async_send(boost::asio::buffer(request.data(), request.size()), [this, request](const boost::system::error_code& ec, std::size_t bytes_transferred) mutable { @@ -124,7 +184,7 @@ public: } /// @brief Receive response from the server. - void receivePartialResponse() { + virtual void receivePartialResponse() { socket_.async_read_some(boost::asio::buffer(buf_.data(), buf_.size()), [this](const boost::system::error_code& ec, std::size_t bytes_transferred) { @@ -171,7 +231,7 @@ public: /// please use @c isConnectionClosed instead. /// /// @return true if the TCP connection is open. - bool isConnectionAlive() { + virtual bool isConnectionAlive() { // Remember the current non blocking setting. const bool non_blocking_orig = socket_.non_blocking(); // Set the socket to non blocking mode. We're going to test if the socket @@ -204,7 +264,7 @@ public: /// use @c isConnectionAlive. /// /// @return true if the TCP connection is closed. - bool isConnectionClosed() { + virtual bool isConnectionClosed() { // Remember the current non blocking setting. const bool non_blocking_orig = socket_.non_blocking(); // Set the socket to blocking mode. We're going to test if the socket @@ -224,14 +284,14 @@ public: } /// @brief Close connection. - void close() { + virtual void close() { socket_.close(); } /// @brief Returns the HTTP response string. /// /// @return string containing the response. - std::string getResponse() const { + virtual std::string getResponse() const { return (response_); } @@ -239,7 +299,7 @@ public: /// /// @return True if the receive completed successfully, false /// otherwise. - bool receiveDone() { + virtual bool receiveDone() const { return (receive_done_); } @@ -270,4 +330,268 @@ private: /// @brief Pointer to the TestHttpClient. typedef boost::shared_ptr TestHttpClientPtr; +/// @brief Entity which can connect to the HTTPS server endpoint. +class TestHttpsClient : public boost::noncopyable { +public: + + /// @brief Constructor. + /// + /// This constructor creates new socket instance. It doesn't connect. Call + /// connect() to connect to the server. + /// + /// @param io_service IO service to be stopped on error. + /// @param tls_context TLS context. + /// @param server_address string containing the IP address of the server. + /// @param port port number of the server. + TestHttpsClient(const IOServicePtr& io_service, TlsContextPtr tls_context, + const std::string& server_address = "127.0.0.1", + uint16_t port = 18123) + : io_service_(io_service), stream_(io_service_->getInternalIOService(), + tls_context->getContext()), buf_(), response_(), + server_address_(server_address), server_port_(port), + receive_done_(false) { + } + + /// @brief Destructor. + /// + /// Closes the underlying socket if it is open. + virtual ~TestHttpsClient() { + close(); + } + + /// @brief Send HTTP request specified in textual format. + /// + /// @param request HTTP request in the textual format. + virtual void startRequest(const std::string& request) { + tcp::endpoint endpoint(address::from_string(server_address_), + server_port_); + stream_.lowest_layer().async_connect(endpoint, + [this, request](const boost::system::error_code& ec) { + receive_done_ = false; + if (ec) { + // One would expect that async_connect wouldn't return + // EINPROGRESS error code, but simply wait for the connection + // to get established before the handler is invoked. It turns out, + // however, that on some OSes the connect handler may receive this + // error code which doesn't necessarily indicate a problem. + // Making an attempt to write and read from this socket will + // typically succeed. So, we ignore this error. + if (ec.value() != boost::asio::error::in_progress) { + ADD_FAILURE() << "error occurred while connecting: " + << ec.message(); + io_service_->stop(); + return; + } + } + stream_.async_handshake(roleToImpl(TlsRole::CLIENT), + [this, request](const boost::system::error_code& ec) { + if (ec) { + ADD_FAILURE() << "error occurred during handshake: " + << ec.message(); + io_service_->stop(); + return; + } + sendRequest(request); + }); + }); + } + + /// @brief Send HTTP request. + /// + /// @param request HTTP request in the textual format. + virtual void sendRequest(const std::string& request) { + sendPartialRequest(request); + } + + /// @brief Send part of the HTTP request. + /// + /// @param request part of the HTTP request to be sent. + virtual void sendPartialRequest(std::string request) { + boost::asio::async_write(stream_, + boost::asio::buffer(request.data(), request.size()), + [this, request](const boost::system::error_code& ec, + std::size_t bytes_transferred) mutable { + if (ec) { + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + } else if ((ec.value() == boost::asio::error::try_again) || + (ec.value() == boost::asio::error::would_block)) { + // If we should try again make sure there is no garbage in the + // bytes_transferred. + bytes_transferred = 0; + + } else { + ADD_FAILURE() << "error occurred while connecting: " + << ec.message(); + io_service_->stop(); + return; + } + } + + // Remove the part of the request which has been sent. + if (bytes_transferred > 0 && (request.size() <= bytes_transferred)) { + request.erase(0, bytes_transferred); + } + + // Continue sending request data if there are still some data to be + // sent. + if (!request.empty()) { + sendPartialRequest(request); + + } else { + // Request has been sent. Start receiving response. + response_.clear(); + receivePartialResponse(); + } + }); + } + + /// @brief Receive response from the server. + virtual void receivePartialResponse() { + stream_.async_read_some(boost::asio::buffer(buf_.data(), buf_.size()), + [this](const boost::system::error_code& ec, + std::size_t bytes_transferred) { + if (ec) { + // IO service stopped so simply return. + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + } else if ((ec.value() == boost::asio::error::try_again) || + (ec.value() == boost::asio::error::would_block)) { + // If we should try again, make sure that there is no garbage + // in the bytes_transferred. + bytes_transferred = 0; + + } else { + // Error occurred, bail... + ADD_FAILURE() << "error occurred while receiving HTTP" + " response from the server: " << ec.message(); + io_service_->stop(); + } + } + + if (bytes_transferred > 0) { + response_.insert(response_.end(), buf_.data(), + buf_.data() + bytes_transferred); + } + + // Two consecutive new lines end the part of the response we're + // expecting. + if (response_.find("\r\n\r\n", 0) != std::string::npos) { + receive_done_ = true; + io_service_->stop(); + } else { + receivePartialResponse(); + } + + }); + } + + /// @brief Checks if the TCP connection is still open. + /// + /// Tests the TCP connection by trying to read from the socket. + /// Unfortunately expected failure depends on a race between the read + /// and the other side close so to check if the connection is closed + /// please use @c isConnectionClosed instead. + /// + /// @return true if the TCP connection is open. + virtual bool isConnectionAlive() { + // Remember the current non blocking setting. + const bool non_blocking_orig = stream_.lowest_layer().non_blocking(); + // Set the socket to non blocking mode. We're going to test if the socket + // returns would_block status on the attempt to read from it. + stream_.lowest_layer().non_blocking(true); + + // We need to provide a buffer for a call to read. + char data[2]; + boost::system::error_code ec; + boost::asio::read(stream_, boost::asio::buffer(data, sizeof(data)), ec); + + // Revert the original non_blocking flag on the socket. + stream_.lowest_layer().non_blocking(non_blocking_orig); + + // If the connection is alive we'd typically get would_block status code. + // If there are any data that haven't been read we may also get success + // status. We're guessing that try_again may also be returned by some + // implementations in some situations. Any other error code indicates a + // problem with the connection so we assume that the connection has been + // closed. + return (!ec || (ec.value() == boost::asio::error::try_again) || + (ec.value() == boost::asio::error::would_block)); + } + + /// @brief Checks if the TCP connection is already closed. + /// + /// Tests the TCP connection by trying to read from the socket. + /// The read can block so this must be used to check if a connection + /// is alive so to check if the connection is alive please always + /// use @c isConnectionAlive. + /// + /// @return true if the TCP connection is closed. + virtual bool isConnectionClosed() { + // Remember the current non blocking setting. + const bool non_blocking_orig = stream_.lowest_layer().non_blocking(); + // Set the socket to blocking mode. We're going to test if the socket + // returns eof status on the attempt to read from it. + stream_.lowest_layer().non_blocking(false); + + // We need to provide a buffer for a call to read. + char data[2]; + boost::system::error_code ec; + boost::asio::read(stream_, boost::asio::buffer(data, sizeof(data)), ec); + + // Revert the original non_blocking flag on the socket. + stream_.lowest_layer().non_blocking(non_blocking_orig); + + // If the connection is closed we'd typically get eof or + // stream_truncated status code. + return ((ec.value() == boost::asio::error::eof) || + (ec.value() == STREAM_TRUNCATED)); + } + + /// @brief Close connection. + virtual void close() { + stream_.lowest_layer().close(); + } + + virtual std::string getResponse() const { + return (response_); + } + + /// @brief Returns true if the receive completed without error. + /// + /// @return True if the receive completed successfully, false + /// otherwise. + virtual bool receiveDone() const { + return (receive_done_); + } + +private: + + /// @brief Holds pointer to the IO service. + isc::asiolink::IOServicePtr io_service_; + + /// @brief A socket used for the connection. + TlsStreamImpl stream_; + + /// @brief Buffer into which response is written. + std::array buf_; + + /// @brief Response in the textual format. + std::string response_; + + /// @brief IP address of the server. + std::string server_address_; + + /// @brief IP port of the server. + uint16_t server_port_; + + /// @brief Set to true when the receive has completed successfully. + bool receive_done_; +}; + +/// @brief Pointer to the TestHttpsClient. +typedef boost::shared_ptr TestHttpsClientPtr; + #endif