#include <config.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <cc/command_interpreter.h>
#include <config/cmd_http_listener.h>
+#include <config/command_mgr.h>
+#include <http/response.h>
+#include <http/response_parser.h>
+#include <http/tests/test_http_client.h>
+#include <util/multi_threading_mgr.h>
#include <testutils/gtest_utils.h>
#include <gtest/gtest.h>
+#include <thread>
+#include <list>
+#include <sstream>
+
using namespace isc;
using namespace isc::config;
using namespace isc::data;
+using namespace boost::asio::ip;
+using namespace isc::asiolink;
+using namespace isc::http;
+using namespace isc::util;
+namespace ph = std::placeholders;
namespace {
-/// Verifies the construction, starting, stopping, destruction
-/// the CmdHttpListener class.
+/// @brief IP address to which HTTP service is bound.
+const std::string SERVER_ADDRESS = "127.0.0.1";
+
+/// @brief Port number to which HTTP service is bound.
+const unsigned short SERVER_PORT = 18123;
+
+/// @brief Test timeout (ms).
+const long TEST_TIMEOUT = 10000;
+
+/// Verifies the construction, starting, stopping, and destruction
+/// of CmdHttpListener.
TEST(CmdHttpListener, basics) {
CmdHttpListenerPtr listener;
- asiolink::IOAddress address("127.0.0.1");
- uint16_t port = 8080;
+ asiolink::IOAddress address(SERVER_ADDRESS);
+ uint16_t port = SERVER_PORT;
// Make sure we can create one.
ASSERT_NO_THROW_LOG(listener.reset(new CmdHttpListener(address, port)));
EXPECT_FALSE(listener->isListening());
EXPECT_EQ(listener->getThreadCount(), 0);
+ // Verify that we cannot start it when multi-threading is disabled.
+ ASSERT_FALSE(MultiThreadingMgr::instance().getMode());
+ ASSERT_THROW_MSG(listener->start(), InvalidOperation,
+ "CmdHttpListener cannot be started"
+ " when multi-threading is disabled");
+
+ // It should still not be listening and have no threads.
+ EXPECT_FALSE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 0);
+
+ // Enable multi-threading.
+ MultiThreadingMgr::instance().setMode(true);
+
// Make sure we can start it and it's listening with 1 thread.
ASSERT_NO_THROW_LOG(listener->start());
ASSERT_TRUE(listener->isListening());
EXPECT_EQ(listener->getThreadCount(), 0);
}
+/// @brief Test fixture class for @ref CmdHttpListener.
+class CmdHttpListenerTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Starts test timer which detects timeouts, deregisters all commands
+ /// from CommandMgr, and enables multi-threading mode.
+ CmdHttpListenerTest()
+ : io_service_(), test_timer_(io_service_), run_io_service_timer_(io_service_),
+ clients_(), num_threads_(), num_clients_() {
+ test_timer_.setup(std::bind(&CmdHttpListenerTest::timeoutHandler, this, true),
+ TEST_TIMEOUT, IntervalTimer::ONE_SHOT);
+
+ // Deregisters commands.
+ CommandMgr::instance().deregisterAll();
+
+ // Ensure we're in MT mode.
+ MultiThreadingMgr::instance().setMode(true);
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Removes HTTP clients, unregisters commands, disables MT.
+ virtual ~CmdHttpListenerTest() {
+ // Destroy all remaining clients.
+ for (auto client = clients_.begin(); client != clients_.end();
+ ++client) {
+ (*client)->close();
+ }
+
+ // Deregisters commands.
+ config::CommandMgr::instance().deregisterAll();
+
+ // Shut of MT.
+ MultiThreadingMgr::instance().setMode(false);
+ }
+
+ std::string buildPostStr(const std::string& request_body) {
+ // Create the command string.
+ std::stringstream ss;
+ ss << "POST /foo/bar HTTP/1.1\r\n"
+ "Content-Type: application/json\r\n"
+ "Content-Length: "
+ << request_body.size()<< "\r\n\r\n"
+ << request_body;
+ return (ss.str());
+ }
+
+ /// @brief Initiates a command via a new HTTP client.
+ ///
+ /// This method creates a TestHttpClient instance, adds the
+ /// client to the list of clients, and starts a request based
+ /// on the given command. The client will run on the main
+ /// thread and be driven by the test's IOService instance.
+ ///
+ /// @param request_body JSON String containing the API command
+ /// to be be sent.
+ void startRequest(const std::string& request_body = "{ }") {
+ std::string request_str = buildPostStr(request_body);
+
+ // Instantiate the client.
+ TestHttpClientPtr client(new TestHttpClient(io_service_, SERVER_ADDRESS,
+ SERVER_PORT));
+ // Add it to the list of clients.
+ clients_.push_back(client);
+
+ // Start the request. Note, nothing happens until the IOService runs.
+ client->startRequest(request_str);
+ }
+
+ /// @brief Initiates a "thread" command via a new HTTP client.
+ ///
+ /// This method creates a TestHttpClient instance, adds the
+ /// client to the list of clients, and starts a request based
+ /// on the given command. The client will run on the main
+ /// thread and be driven by the test's IOService instance.
+ ///
+ /// The command has a single argument, "client-ptr". The function creates
+ /// the value for this argument from the pointer address of client instance
+ /// it creates. This argument should be echoed back in the response, along
+ /// with the thread-id of the CmdHttpListener thread which handled the
+ /// command. The response body should look this:
+ ///
+ /// ```
+ /// [ { "arguments": { "client-ptr": "xxxxx", "thread-id": "zzzzz" }, "result": 0} ]
+ /// ```
+ void startThreadCommand() {
+ // Create a new client.
+ TestHttpClientPtr client(new TestHttpClient(io_service_, SERVER_ADDRESS,
+ SERVER_PORT));
+
+ // Construct the "thread" command post including the argument,
+ // "client-ptr", whose value is the stringified pointer to the
+ // newly created client.
+ std::stringstream request_body;
+ request_body << "{\"command\": \"thread\", \"arguments\": { \"client-ptr\": \""
+ << client << "\" } }";
+
+ std::string command = buildPostStr(request_body.str());
+
+ // Add it to the list of clients.
+ clients_.push_back(client);
+
+ // Start the request. Note, nothing happens until the IOService runs.
+ ASSERT_NO_THROW_LOG(client->startRequest(command));
+ }
+
+ /// @brief Callback function invoke upon test timeout.
+ ///
+ /// It stops the IO service and reports test timeout.
+ ///
+ /// @param fail_on_timeout Specifies if test failure should be reported.
+ void timeoutHandler(const bool fail_on_timeout) {
+ if (fail_on_timeout) {
+ ADD_FAILURE() << "Timeout occurred while running the test!";
+ }
+ io_service_.stop();
+ }
+
+ /// @brief Runs IO service with optional timeout.
+ ///
+ /// We iterate over calls to asio::io_service.run(), until
+ /// all the clients have completed their requests. We do it this
+ /// because the test clients stop the io_service when they're
+ /// through with a request.
+ ///
+ /// @param timeout Optional value specifying for how long the io service
+ /// should be ran.
+ void runIOService() {
+ // Loop until the clients are done, an error occurs, or the time runs out.
+ bool keep_going = true;
+ while (keep_going) {
+ // Always call restart() before we call run();
+ io_service_.get_io_service().restart();
+
+ // Run until a client stops the service.
+ io_service_.run();
+
+ // If all the clients are done receiving, the test is done.
+ keep_going = false;
+ for ( auto client : clients_ ) {
+ if (!client->receiveDone()) {
+ keep_going = true;
+ break;
+ }
+ }
+ }
+ }
+
+ /// @brief Create an HttpResponse from a response string.
+ ///
+ /// @param response_str a string containing the whole HTTP
+ /// response received.
+ ///
+ /// @return An HttpResponse constructed from by parsing the
+ /// response string.
+ HttpResponsePtr parseResponse(const std::string response_str) {
+ HttpResponsePtr hr(new HttpResponse());
+ HttpResponseParser parser(*hr);
+ parser.initModel();
+ parser.postBuffer(&response_str[0], response_str.size());
+ parser.poll();
+ if (!parser.httpParseOk()) {
+ isc_throw(Unexpected, "response_str: '" << response_str
+ << "' failed to parse: " << parser.getErrorMessage());
+ }
+
+ return (hr);
+ }
+
+ /// @brief Handler for the 'foo' command.
+ ///
+ /// The command needs no arguments and returns a response
+ /// with a body containing:
+ ///
+ /// "[ { \"arguments\": [ \"bar\" ], \"result\": 0 } ]")
+ ///
+ /// @param command_name Command name, i.e. 'foo'.
+ /// @param command_arguments Command arguments (empty).
+ ///
+ /// @return Returns response with a single string "bar".
+ ConstElementPtr fooCommandHandler(const std::string& /*command_name*/,
+ const ConstElementPtr& /*command_arguments*/) {
+ ElementPtr arguments = Element::createList();
+ arguments->add(Element::create("bar"));
+ return (createAnswer(CONTROL_RESULT_SUCCESS, arguments));
+ }
+
+ /// @brief Handler for the 'thread' command.
+ ///
+ /// @param command_name Command name, i.e. 'thread'.
+ /// @param command_arguments Command arguments should contain
+ /// one string element, "client-ptr", whose value is the stringified
+ /// pointer to the client that issued the command.
+ ///
+ /// @return Returns response with map of arguments containing
+ /// a string value 'thread-id': <thread id>
+ ConstElementPtr threadCommandHandler(const std::string& /*command_name*/,
+ const ConstElementPtr& command_arguments) {
+ // If the number of in progress commands is less than the number
+ // of threads, then wait here until we're notified. Otherwise,
+ // notify everyone and finish. The idea is to force each thread
+ // to handle the same number of requests over the course of the
+ // test, making verification reliable.
+ if (num_clients_ > 1) {
+ std::unique_lock<std::mutex> lck(mutex_);
+ ++num_in_progress_;
+ if (num_in_progress_ < num_threads_) {
+ cv_.wait(lck);
+ } else {
+ num_in_progress_ = 0;
+ cv_.notify_all();
+ }
+ }
+
+ // Create the map of response arguments.
+ ElementPtr arguments = Element::createMap();
+ // First we echo the client-ptr command argument.
+ ConstElementPtr client_ptr = command_arguments->get("client-ptr");
+ if (!client_ptr) {
+ return (createAnswer(CONTROL_RESULT_ERROR, "missing client-ptr"));
+ }
+
+ arguments->set("client-ptr", client_ptr);
+
+ // Now we add the thread-id.
+ std::stringstream ss;
+ ss << std::this_thread::get_id();
+ arguments->set("thread-id", Element::create(ss.str()));
+
+ // We're done, ship it!
+ return (createAnswer(CONTROL_RESULT_SUCCESS, arguments));
+ }
+
+ /// @brief Submits one or more thread commands to a CmdHttpListener
+ ///
+ /// This function command will creates a CmdHttpListener
+ /// with the given number of threads, initiates the given
+ /// number of clients, each requesting the "thread" command,
+ /// and then iteratively runs the test's IOService until all
+ /// the clients have received their responses or an error occurs.
+ ///
+ /// It requires that the number of clients, when greater than the
+ /// number of threads, be a multiple of the number of threads. The
+ /// thread command handler is structured in such a way as to ensure
+ /// (we hope) that each thread handles the same number of commands.
+ ///
+ /// @param num_threads - the number of threads the CmdHttpListener
+ /// should use. Must be greater than 0.
+ /// @param num_clients - the number of clients that should issue the
+ /// thread command. Each client is used to carry out a single thread
+ /// command request. Must be greater than 0 and a multiple of num_threads
+ /// if it is greater than num_threads.
+ ///
+ void threadListenAndRespond(size_t num_threads, size_t num_clients) {
+ // First we makes sure the parameter rules apply.
+ ASSERT_TRUE(num_threads > 0);
+ ASSERT_TRUE(num_clients > 0);
+ ASSERT_TRUE((num_clients < num_threads) || (num_clients % num_threads == 0));
+
+ num_threads_ = num_threads;
+ num_clients_ = num_clients;
+
+ // Register the thread command handler.
+ CommandMgr::instance().registerCommand("thread",
+ std::bind(&CmdHttpListenerTest
+ ::threadCommandHandler,
+ this, ph::_1, ph::_2));
+
+ // Create a listener with prescribed number of threads.
+ CmdHttpListenerPtr listener;
+ ASSERT_NO_THROW_LOG(listener.reset(new CmdHttpListener(IOAddress(SERVER_ADDRESS),
+ SERVER_PORT, num_threads)));
+ ASSERT_TRUE(listener);
+
+ // Start it and verify it is listening.
+ ASSERT_NO_THROW_LOG(listener->start());
+ ASSERT_TRUE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), num_threads);
+
+ // Maps the number of clients served by a given thread-id.
+ std::map<std::string, int> clients_per_thread;
+
+ // Initiate the prescribed number of command requests.
+ num_in_progress_ = 0;
+ for ( auto i = 0; clients_.size() < num_clients; ++i) {
+ ASSERT_NO_THROW_LOG(startThreadCommand());
+ }
+
+ // Now we run the client-side IOService until all requests are done,
+ // errors occur or the test times out.
+ ASSERT_NO_FATAL_FAILURE(runIOService());
+
+ // Stop the listener and then verify it has stopped.
+ ASSERT_NO_THROW_LOG(listener->stop());
+ ASSERT_FALSE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 0);
+
+ // Iterate over the clients, checking their outcomes.
+ size_t total_responses = 0;
+ for (auto client : clients_) {
+ // Client should have completed its receive successfully.
+ ASSERT_TRUE(client->receiveDone());
+
+ // Client response should not be empty.
+ HttpResponsePtr hr;
+ std::string response_str = client->getResponse();
+ ASSERT_FALSE(response_str.empty());
+
+ // Parse the response into an HttpResponse.
+ ASSERT_NO_THROW_LOG(hr = parseResponse(client->getResponse()));
+
+ // Now we walk the element tree to get the response data. It should look
+ // this:
+ //
+ // [ {
+ // "arguments": { "client-ptr": "xxxxx",
+ // "thread-id": "zzzzz" },
+ // "result": 0
+ // } ]
+ //
+ // First we turn it into an Element tree.
+ std::string body_str = hr->getBody();
+ ConstElementPtr body;
+ ASSERT_NO_THROW_LOG(body = Element::fromJSON(hr->getBody()));
+
+ // Outermost is a list, since we're emulating agent responses.
+ ASSERT_EQ(body->getType(), Element::list);
+ ASSERT_EQ(body->size(), 1);
+
+ // Answer should be a map containing "arguments" and "results".
+ ConstElementPtr answer = body->get(0);
+ ASSERT_EQ(answer->getType(), Element::map);
+
+ // "result" should be 0.
+ ConstElementPtr result = answer->get("result");
+ ASSERT_TRUE(result);
+ ASSERT_EQ(result->getType(), Element::integer);
+ ASSERT_EQ(result->intValue(), 0);
+
+ // "arguments" is a map containing "client-ptr" and "thread-id".
+ ConstElementPtr arguments = answer->get("arguments");
+ ASSERT_TRUE(arguments);
+ ASSERT_EQ(arguments->getType(), Element::map);
+
+ // "client-ptr" is a string.
+ ConstElementPtr client_ptr = arguments->get("client-ptr");
+ ASSERT_TRUE(client_ptr);
+ ASSERT_EQ(client_ptr->getType(), Element::string);
+
+ // "thread-id" is a string.
+ ConstElementPtr thread_id = arguments->get("thread-id");
+ ASSERT_TRUE(thread_id);
+ ASSERT_EQ(thread_id->getType(), Element::string);
+ std::string thread_id_str = thread_id->stringValue();
+
+ // Make sure the response received was for this client.
+ std::stringstream ss;
+ ss << client;
+ ASSERT_EQ(client_ptr->stringValue(), ss.str());
+
+ // Bump the client count for the given thread-id.
+ auto it = clients_per_thread.find(thread_id_str);
+ if (it != clients_per_thread.end()) {
+ clients_per_thread[thread_id_str] = it->second + 1;
+ } else {
+ clients_per_thread[thread_id_str] = 1;
+ }
+
+ ++total_responses;
+ }
+
+ // We should have responses for all our clients.
+ EXPECT_EQ(total_responses, num_clients);
+
+ // Verify we have the expected number of entries in our map.
+ size_t expected_thread_count = (num_clients < num_threads ?
+ num_clients : num_threads);
+
+ ASSERT_EQ(clients_per_thread.size(), expected_thread_count);
+
+ // Each thread-id ought to have handled the same number of clients.
+ for (auto it : clients_per_thread) {
+ EXPECT_EQ(it.second, num_clients / clients_per_thread.size())
+ << "thread-id: " << it.first
+ << ", clients: " << it.second << std::endl;
+ }
+ }
+
+ /// @brief IO service used in drive the test and test clients.
+ IOService io_service_;
+
+ /// @brief Asynchronous timer service to detect timeouts.
+ IntervalTimer test_timer_;
+
+ /// @brief Asynchronous timer for running IO service for a specified amount
+ /// of time.
+ IntervalTimer run_io_service_timer_;
+
+ /// @brief List of client connections.
+ std::list<TestHttpClientPtr> clients_;
+
+ size_t num_threads_;
+ size_t num_clients_;
+ size_t num_in_progress_;
+ std::mutex mutex_;
+ std::condition_variable cv_;
+};
+
+// This test verifies that an HTTP connection can be established and used to
+// transmit an HTTP request and receive the response.
+TEST_F(CmdHttpListenerTest, basicListenAndRespond) {
+
+ // Create a listener with 1 thread.
+ CmdHttpListenerPtr listener;
+ ASSERT_NO_THROW_LOG(listener.reset(new CmdHttpListener(IOAddress(SERVER_ADDRESS),
+ SERVER_PORT)));
+ ASSERT_TRUE(listener);
+
+ // Start the listener and verify it's listening with 1 thread.
+ ASSERT_NO_THROW_LOG(listener->start());
+ ASSERT_TRUE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 1);
+
+ // Now let's send a "foo" command. This should create a client, connect
+ // to our listener, post our request and retrieve our reply.
+ ASSERT_NO_THROW(startRequest("{\"command\": \"foo\"}"));
+ ASSERT_EQ(1, clients_.size());
+ ASSERT_NO_THROW(runIOService());
+ TestHttpClientPtr client = clients_.front();
+ ASSERT_TRUE(client);
+
+ // Parse the response into an HttpResponse.
+ HttpResponsePtr hr;
+ ASSERT_NO_THROW_LOG(hr = parseResponse(client->getResponse()));
+
+ // Without a command handler loaded, we should get an unsupported command response.
+ EXPECT_EQ(hr->getBody(), "[ { \"result\": 2, \"text\": \"'foo' command not supported.\" } ]");
+
+ // Now let's register the foo command handler.
+ CommandMgr::instance().registerCommand("foo",
+ std::bind(&CmdHttpListenerTest::fooCommandHandler,
+ this, ph::_1, ph::_2));
+ // Try posting the foo command again.
+ ASSERT_NO_THROW(startRequest("{\"command\": \"foo\"}"));
+ ASSERT_EQ(2, clients_.size());
+ ASSERT_NO_THROW(runIOService());
+ client = clients_.back();
+ ASSERT_TRUE(client);
+
+ // Parse the response.
+ ASSERT_NO_THROW_LOG(hr = parseResponse(client->getResponse()));
+
+ // We should have a response from our command handler.
+ EXPECT_EQ(hr->getBody(), "[ { \"arguments\": [ \"bar\" ], \"result\": 0 } ]");
+
+ // Make sure the listener is still listening.
+ ASSERT_TRUE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 1);
+
+ // Stop the listener then verify it has stopped.
+ ASSERT_NO_THROW_LOG(listener->stop());
+ ASSERT_FALSE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 0);
+}
+
+// Now we'll run some permutations of the number of listener threads
+// and the number of client requests.
+
+// One thread, one client.
+TEST_F(CmdHttpListenerTest, oneByOne) {
+ size_t num_threads = 1;
+ size_t num_clients = 1;
+ threadListenAndRespond(num_threads, num_clients);
+}
+
+// One thread, four clients.
+TEST_F(CmdHttpListenerTest, oneByFour) {
+ size_t num_threads = 1;
+ size_t num_clients = 4;
+ threadListenAndRespond(num_threads, num_clients);
+}
+
+// Four threads, one clients.
+TEST_F(CmdHttpListenerTest, fourByOne) {
+ size_t num_threads = 4;
+ size_t num_clients = 1;
+ threadListenAndRespond(num_threads, num_clients);
+}
+
+// Four threads, four clients.
+TEST_F(CmdHttpListenerTest, fourByFour) {
+ size_t num_threads = 4;
+ size_t num_clients = 4;
+ threadListenAndRespond(num_threads, num_clients);
+}
+
+// Four threads, eight clients.
+TEST_F(CmdHttpListenerTest, fourByEight) {
+ size_t num_threads = 4;
+ size_t num_clients = 8;
+ threadListenAndRespond(num_threads, num_clients);
+}
+
+// Six threads, eighteen clients.
+TEST_F(CmdHttpListenerTest, sixByEighteen) {
+ size_t num_threads = 6;
+ size_t num_clients = 18;
+ threadListenAndRespond(num_threads, num_clients);
+}
+
} // end of anonymous namespace
#include <asiolink/asio_wrapper.h>
#include <asiolink/interval_timer.h>
#include <cc/data.h>
+#include <test_http_client.h>
#include <http/client.h>
#include <http/http_types.h>
#include <http/listener.h>
}
};
-
-/// @brief Entity which can connect to the HTTP server endpoint.
-class TestHttpClient : 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.
- explicit TestHttpClient(IOService& io_service)
- : io_service_(io_service.get_io_service()), socket_(io_service_),
- buf_(), response_() {
- }
-
- /// @brief Destructor.
- ///
- /// Closes the underlying socket if it is open.
- ~TestHttpClient() {
- 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);
- socket_.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;
- }
- }
- 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) {
- socket_.async_send(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() {
- socket_.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 = socket_.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.
- socket_.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(socket_, boost::asio::buffer(data, sizeof(data)), ec);
-
- // Revert the original non_blocking flag on the socket.
- socket_.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 = socket_.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.
- socket_.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(socket_, boost::asio::buffer(data, sizeof(data)), ec);
-
- // Revert the original non_blocking flag on the socket.
- socket_.non_blocking(non_blocking_orig);
-
- // If the connection is closed we'd typically get eof status code.
- return (ec.value() == boost::asio::error::eof);
- }
-
- /// @brief Close connection.
- void close() {
- socket_.close();
- }
-
- std::string getResponse() const {
- return (response_);
- }
-
-private:
-
- /// @brief Holds reference to the IO service.
- boost::asio::io_service& io_service_;
-
- /// @brief A socket used for the connection.
- boost::asio::ip::tcp::socket socket_;
-
- /// @brief Buffer into which response is written.
- std::array<char, 8192> buf_;
-
- /// @brief Response in the textual format.
- std::string response_;
-};
-
/// @brief Pointer to the TestHttpClient.
typedef boost::shared_ptr<TestHttpClient> TestHttpClientPtr;
--- /dev/null
+// Copyright (C) 2017-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef TEST_HTTP_CLIENT_H
+#define TEST_HTTP_CLIENT_H
+
+#include <cc/data.h>
+#include <http/client.h>
+#include <http/http_types.h>
+
+#include <boost/asio/read.hpp>
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <gtest/gtest.h>
+
+using namespace boost::asio::ip;
+using namespace isc::asiolink;
+
+/// @brief Entity which can connect to the HTTP server endpoint.
+class TestHttpClient : 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 or completion.
+ explicit TestHttpClient(IOService& io_service,
+ const std::string& server_address = "127.0.0.1",
+ uint16_t port = 18123)
+ : io_service_(io_service.get_io_service()), socket_(io_service_),
+ buf_(), response_(), server_address_(server_address),
+ server_port_(port), receive_done_(false) {
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Closes the underlying socket if it is open.
+ ~TestHttpClient() {
+ 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_);
+ socket_.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;
+ }
+ }
+ 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) {
+ socket_.async_send(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() {
+ socket_.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) {
+ std::cout << "this: " << this << "IO service stopped" << std::endl;
+ 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.
+ 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
+ // returns would_block status on the attempt to read from it.
+ socket_.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(socket_, boost::asio::buffer(data, sizeof(data)), ec);
+
+ // Revert the original non_blocking flag on the socket.
+ socket_.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 = socket_.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.
+ socket_.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(socket_, boost::asio::buffer(data, sizeof(data)), ec);
+
+ // Revert the original non_blocking flag on the socket.
+ socket_.non_blocking(non_blocking_orig);
+
+ // If the connection is closed we'd typically get eof status code.
+ return (ec.value() == boost::asio::error::eof);
+ }
+
+ /// @brief Close connection.
+ void close() {
+ socket_.close();
+ }
+
+ std::string getResponse() const {
+ return (response_);
+ }
+
+ /// @brief Returns true if the receive completed without error.
+ bool receiveDone() {
+ return (receive_done_);
+ }
+
+private:
+
+ /// @brief Holds reference to the IO service.
+ boost::asio::io_service& io_service_;
+
+ /// @brief A socket used for the connection.
+ boost::asio::ip::tcp::socket socket_;
+
+ /// @brief Buffer into which response is written.
+ std::array<char, 8192> 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 as completed successfully.
+ bool receive_done_;
+};
+
+/// @brief Pointer to the TestHttpClient.
+typedef boost::shared_ptr<TestHttpClient> TestHttpClientPtr;
+
+#endif