libkea_cfgclient_la_SOURCES += config_messages.h config_messages.cc
libkea_cfgclient_la_SOURCES += hooked_command_mgr.cc hooked_command_mgr.h
libkea_cfgclient_la_SOURCES += timeouts.h
+libkea_cfgclient_la_SOURCES += cmd_http_listener.cc cmd_http_listener.h
+libkea_cfgclient_la_SOURCES += cmd_response_creator.cc cmd_response_creator.h
+libkea_cfgclient_la_SOURCES += cmd_response_creator_factory.h
-libkea_cfgclient_la_LIBADD = $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
+libkea_cfgclient_la_LIBADD = $(top_builddir)/src/lib/http/libkea-http.la
+libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la
libkea_cfgclient_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
--- /dev/null
+// Copyright (C) 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/.
+
+#include <config.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/io_address.h>
+#include <asiolink/io_error.h>
+#include <asiolink/io_service.h>
+#include <cmd_http_listener.h>
+#include <cmd_response_creator_factory.h>
+#include <config_log.h>
+#include <config/timeouts.h>
+
+#include <boost/pointer_cast.hpp>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::http;
+
+namespace isc {
+namespace config {
+
+CmdHttpListener::CmdHttpListener(const IOAddress& address, const uint16_t port,
+ const uint16_t thread_pool_size /* = 1 */)
+ : address_(address), port_(port), http_listener_(), thread_pool_size_(thread_pool_size), threads_() {
+}
+
+CmdHttpListener::~CmdHttpListener() {
+ stop();
+}
+
+void
+CmdHttpListener::start() {
+ if (isListening()) {
+ isc_throw(InvalidOperation, "CmdHttpListener is already listening!");
+ }
+
+ try {
+ // Create a new IOService.
+ io_service_.reset(new IOService());
+
+ // Create the response creator factory first. It will be used to
+ // generate response creators. Each response creator will be
+ // used to generate the answer to specific request.
+ HttpResponseCreatorFactoryPtr rcf(new CmdResponseCreatorFactory());
+
+ // Create the HTTP listener. It will open up a TCP socket and be
+ // prepared to accept incoming connections.
+ http_listener_.reset(new HttpListener(*io_service_, address_, port_, rcf,
+ HttpListener::RequestTimeout(TIMEOUT_AGENT_RECEIVE_COMMAND),
+ HttpListener::IdleTimeout(TIMEOUT_AGENT_IDLE_CONNECTION_TIMEOUT)));
+
+ // Create a pool of threads, each calls run on our IOService_service instance.
+ for (std::size_t i = 0; i < thread_pool_size_; ++i)
+ {
+ boost::shared_ptr<std::thread> thread(new std::thread(
+ std::bind(&IOService::run, io_service_)));
+ threads_.push_back(thread);
+ }
+
+ // Instruct the HTTP listener to actually open socket, install
+ // callback and start listening.
+ http_listener_->start();
+
+ // OK, seems like we're good to go.
+ LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_HTTP_LISTENER_STARTED)
+ .arg(thread_pool_size_)
+ .arg(address_)
+ .arg(port_);
+ } catch (const std::exception& ex) {
+ isc_throw(Unexpected, "CmdHttpListener::run failed:" << ex.what());
+ }
+}
+
+void
+CmdHttpListener::stop() {
+ // Nothing to do.
+ if (!io_service_) {
+ return;
+ }
+
+ LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_HTTP_LISTENER_STOPPING)
+ .arg(address_)
+ .arg(port_);
+
+ // Stop the IOService first.
+ io_service_->stop();
+
+ // Stop the threads next.
+ for (std::size_t i = 0; i < threads_.size(); ++i) {
+ threads_[i]->join();
+ }
+
+ threads_.clear();
+
+ // Get rid of the listener.
+ http_listener_.reset();
+
+ // Ditch the IOService.
+ io_service_.reset();
+
+ LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_HTTP_LISTENER_STOPPED)
+ .arg(address_)
+ .arg(port_);
+}
+
+bool
+CmdHttpListener::isListening() const {
+ // If we have a listener we're listening.
+ return (http_listener_ != 0);
+}
+
+} // namespace isc::config
+} // namespace isc
--- /dev/null
+// Copyright (C) 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 CMD_HTTP_LISTENER_H
+#define CMD_HTTP_LISTENER_H
+
+#include <asiolink/io_address.h>
+#include <asiolink/io_service.h>
+#include <http/listener.h>
+#include <thread>
+#include <vector>
+
+namespace isc {
+namespace config {
+
+class CmdHttpListener {
+public:
+ /// @brief Constructor
+ CmdHttpListener(const asiolink::IOAddress& address, const uint16_t port,
+ const uint16_t thread_pool_size = 1);
+
+ /// @brief Destructor
+ virtual ~CmdHttpListener();
+
+ /// @brief Initiates the listener's worker thread.
+ void start();
+
+ /// @brief Stops the listener's worker thread.
+ void stop();
+
+ /// @brief Checks if we are listening to the HTTP requests.
+ ///
+ /// @return true if we are listening.
+ bool isListening() const;
+
+ /// @brief Fetches the IP address on which to listen.
+ ///
+ /// @return IOAddress containing the address on which to listen.
+ isc::asiolink::IOAddress& getAddress() {
+ return (address_);
+ }
+
+ /// @brief Fetches the port number on which to listen.
+ ///
+ /// @return unit16_t containing the port number on which to listen.
+ uint16_t getPort() {
+ return (port_);
+ }
+
+ /// @brief Fetches the maximum size of the thread pool.
+ ///
+ /// @return unit16_t containing the maximum size of the thread pool.
+ uint16_t getThreadPoolSize() {
+ return (thread_pool_size_);
+ }
+
+ /// @brief Fetches the number of threads in the pool.
+ ///
+ /// @return unit16_t containing the number of running threads.
+ uint16_t getThreadCount() {
+ return (threads_.size());
+ }
+
+private:
+ /// @brief IP address on which to listen.
+ isc::asiolink::IOAddress address_;
+
+ /// @brief Port on which to listen.
+ uint16_t port_;
+
+ /// @brief IOService instance that drives our IO.
+ isc::asiolink::IOServicePtr io_service_;
+
+ /// @brief The HttpListener instance
+ http::HttpListenerPtr http_listener_;
+
+ /// @brief The number of threads that will call IOService_context::run().
+ std::size_t thread_pool_size_;
+
+ /// @brief The pool of threads that do IO work.
+ std::vector<boost::shared_ptr<std::thread> > threads_;
+};
+
+/// @brief Defines a shared pointer to CmdHttpListener.
+typedef boost::shared_ptr<CmdHttpListener> CmdHttpListenerPtr;
+
+}; // namespace isc::config
+}; // namespace isc
+
+#endif // CMD_HTTP_LISTENER_H
--- /dev/null
+// Copyright (C) 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/.
+
+#include <config.h>
+
+#include <config/cmd_response_creator.h>
+#include <config/command_mgr.h>
+#include <cc/data.h>
+#include <http/post_request_json.h>
+#include <http/response_json.h>
+#include <boost/pointer_cast.hpp>
+#include <iostream>
+
+using namespace isc::data;
+using namespace isc::http;
+
+namespace isc {
+namespace config {
+
+HttpRequestPtr
+CmdResponseCreator::createNewHttpRequest() const {
+ return (HttpRequestPtr(new PostHttpRequestJson()));
+}
+
+HttpResponsePtr
+CmdResponseCreator::
+createStockHttpResponse(const HttpRequestPtr& request,
+ const HttpStatusCode& status_code) const {
+ HttpResponsePtr response = createStockHttpResponseInternal(request, status_code);
+ response->finalize();
+ return (response);
+}
+
+HttpResponsePtr
+CmdResponseCreator::
+createStockHttpResponseInternal(const HttpRequestPtr& request,
+ const HttpStatusCode& status_code) const {
+ // The request hasn't been finalized so the request object
+ // doesn't contain any information about the HTTP version number
+ // used. But, the context should have this data (assuming the
+ // HTTP version is parsed OK).
+ HttpVersion http_version(request->context()->http_version_major_,
+ request->context()->http_version_minor_);
+ // We only accept HTTP version 1.0 or 1.1. If other version number is found
+ // we fall back to HTTP/1.0.
+ if ((http_version < HttpVersion(1, 0)) || (HttpVersion(1, 1) < http_version)) {
+ http_version.major_ = 1;
+ http_version.minor_ = 0;
+ }
+ // This will generate the response holding JSON content.
+ HttpResponsePtr response(new HttpResponseJson(http_version, status_code));
+ return (response);
+}
+
+HttpResponsePtr
+CmdResponseCreator::
+createDynamicHttpResponse(HttpRequestPtr request) {
+ HttpResponseJsonPtr http_response;
+
+ /// @todo getAuthConfig currently always returns an empty config.
+ const HttpAuthConfigPtr& auth_config = getHttpAuthConfig();
+ if (auth_config) {
+ http_response = auth_config->checkAuth(*this, request);
+ if (http_response) {
+ return (http_response);
+ }
+ }
+
+ // The request is always non-null, because this is verified by the
+ // createHttpResponse method. Let's try to convert it to the
+ // PostHttpRequestJson type as this is the type generated by the
+ // createNewHttpRequest. If the conversion result is null it means that
+ // the caller did not use createNewHttpRequest method to create this
+ // instance. This is considered an error in the server logic.
+ PostHttpRequestJsonPtr request_json = boost::dynamic_pointer_cast<
+ PostHttpRequestJson>(request);
+ if (!request_json) {
+ // Notify the client that we have a problem with our server.
+ return (createStockHttpResponse(request, HttpStatusCode::INTERNAL_SERVER_ERROR));
+ }
+
+ // We have already checked that the request is finalized so the call
+ // to getBodyAsJson must not trigger an exception.
+ ConstElementPtr command = request_json->getBodyAsJson();
+
+ // Process command doesn't generate exceptions but can possibly return
+ // null response, if the handler is not implemented properly. This is
+ // again an internal server issue.
+ ConstElementPtr response = config::CommandMgr::instance().processCommand(command);
+
+ if (!response) {
+ // Notify the client that we have a problem with our server.
+ return (createStockHttpResponse(request, HttpStatusCode::INTERNAL_SERVER_ERROR));
+ }
+
+ // Normal Responses coming from the Kea Control Agent must always be wrapped in
+ // a list as they may contain responses from multiple daemons.
+ // If we're emulating that for backward compatibility, then we need to wrap
+ // the answer in a list if it isn't in one already.
+ if (emulateAgentResponse() && (response->getType() != Element::list)) {
+ ElementPtr response_list = Element::createList();
+ response_list->add(boost::const_pointer_cast<Element>(response));
+ response = response_list;
+ }
+
+ // The response is OK, so let's create new HTTP response with the status OK.
+ http_response = boost::dynamic_pointer_cast<
+ HttpResponseJson>(createStockHttpResponseInternal(request, HttpStatusCode::OK));
+ http_response->setBodyAsJson(response);
+ http_response->finalize();
+
+ return (http_response);
+}
+
+} // end of namespace isc::config
+} // end of namespace isc
--- /dev/null
+// Copyright (C) 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 CMD__RESPONSE_CREATOR_H
+#define CMD__RESPONSE_CREATOR_H
+
+#include <http/response_creator.h>
+#include <http/basic_auth_config.h>
+#include <boost/shared_ptr.hpp>
+
+namespace isc {
+namespace config {
+
+/// @brief Concrete implementation of the HTTP response creator used
+/// for processing API commands
+///
+/// See the documentation of the @ref isc::http::HttpResponseCreator for
+/// the basic information how HTTP response creators are utilized by
+/// the libkea-http library to generate HTTP responses.
+///
+/// This creator expects that received requests are encapsulated in the
+/// @ref isc::http::PostHttpRequestJson objects. The generated responses
+/// are encapsulated in the HttpResponseJson objects.
+///
+/// This class uses @ref CommandMgr singleton to process commands
+/// conveyed in the HTTP body. The JSON responses returned by the manager
+/// are placed in the body of the generated HTTP responses.
+class CmdResponseCreator : public http::HttpResponseCreator {
+public:
+
+ /// @brief Constructor
+ ///
+ /// @param emulate_agent_response if true, responses for normal
+ /// command outcomes are guaranteed to be wrapped in an Element::list.
+ /// This emulates how kea-ctrl-agent forms responses. Defualts to true.
+ CmdResponseCreator(bool emulate_agent_response = true)
+ : emulate_agent_response_(emulate_agent_response){};
+
+ /// @brief Create a new request.
+ ///
+ /// This method creates a bare instance of the @ref
+ /// isc::http::PostHttpRequestJson.
+ ///
+ /// @return Pointer to the new instance of the @ref
+ /// isc::http::PostHttpRequestJson.
+ virtual http::HttpRequestPtr createNewHttpRequest() const;
+
+ /// @brief Creates stock HTTP response.
+ ///
+ /// @param request Pointer to an object representing HTTP request.
+ /// @param status_code Status code of the response.
+ /// @return Pointer to an @ref isc::http::HttpResponseJson object
+ /// representing stock HTTP response.
+ virtual http::HttpResponsePtr
+ createStockHttpResponse(const http::HttpRequestPtr& request,
+ const http::HttpStatusCode& status_code) const;
+
+ /// @brief Fetches the current authentication configuration.
+ ///
+ /// @todo The constructor will have to accept either a pointer
+ /// to the authorization config to use, or a pointer to a function to
+ /// return the authorization config. For now we just return an empty
+ /// pointer.
+ ///
+ /// @return an empty HttpAuthConfigPtr.
+ const http::HttpAuthConfigPtr& getHttpAuthConfig() {
+ static http::HttpAuthConfigPtr no_config;
+ return(no_config);
+ }
+
+ /// @brief Indicates whether or not agent response emulation is enabled.
+ ///
+ /// @return true if emulation is enabled.
+ bool emulateAgentResponse() {
+ return(emulate_agent_response_);
+ }
+
+private:
+
+ /// @brief Creates un-finalized stock HTTP response.
+ ///
+ /// The un-finalized response is the response that can't be sent over the
+ /// wire until @c finalize() is called, which commits the contents of the
+ /// message body.
+ ///
+ /// @param request Pointer to an object representing HTTP request.
+ /// @param status_code Status code of the response.
+ /// @return Pointer to an @ref isc::http::HttpResponseJson object
+ /// representing stock HTTP response.
+ http::HttpResponsePtr
+ createStockHttpResponseInternal(const http::HttpRequestPtr& request,
+ const http::HttpStatusCode& status_code) const;
+
+ /// @brief Creates implementation specific HTTP response.
+ ///
+ /// @param request Pointer to an object representing HTTP request.
+ /// @return Pointer to an object representing HTTP response.
+ virtual http::HttpResponsePtr
+ createDynamicHttpResponse(http::HttpRequestPtr request);
+
+ /// @brief Determines whether or not responses are enclosed in an Element::list.
+ /// Currently kea-ctrl-agent wraps all responses in a list, as it may have
+ /// response from more than one server. If this is true, we'll ensure
+ /// responses (other than error responses) are in a list.
+ bool emulate_agent_response_;
+};
+
+/// @brief Pointer to the @ref CmdResponseCreator.
+typedef boost::shared_ptr<CmdResponseCreator> CmdResponseCreatorPtr;
+
+} // end of namespace isc::config
+} // end of namespace isc
+
+#endif
--- /dev/null
+// Copyright (C) 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 CMD_RESPONSE_CREATOR_FACTORY_H
+#define CMD_RESPONSE_CREATOR_FACTORY_H
+
+#include <config/cmd_response_creator.h>
+#include <http/response_creator_factory.h>
+
+namespace isc {
+namespace config {
+
+/// @brief HTTP response creator factory for an API listener
+///
+/// @param emulate_agent_response if true results for normal command
+/// outcomes are wrapped in Element::list. This emulates responses
+/// generated by kea-ctrl-agent. The value is passed into the
+/// CmdResponseCreator when created. Defaults to true.
+///
+/// See the documentation of the @ref isc::http::HttpResponseCreatorFactory
+/// for the details how the response factory object is used by the
+/// @ref isc::http::HttpListener.
+///
+/// This class always returns the same instance of the
+/// @ref CmdResponseCreator which @ref isc::http::HttpListener and
+/// @ref isc::http::HttpConnection classes use to generate HTTP response
+/// messages which comply with the formats required by the Control Agent.
+class CmdResponseCreatorFactory : public http::HttpResponseCreatorFactory {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Creates sole instance of the @ref CmdResponseCreator object
+ /// returned by the @ref CmdResponseCreatorFactory::create.
+ ///
+ /// @param emulate_agent_response if true, responses for normal
+ /// command outcomes are guaranteed to be wrapped in an Element::list.
+ /// This emulates how kea-ctrl-agent forms responses. Defualts to true.
+ CmdResponseCreatorFactory(bool emulate_agent_response = true)
+ : sole_creator_(new CmdResponseCreator(emulate_agent_response)) {
+ }
+
+ /// @brief Returns an instance of the @ref CmdResponseCreator which
+ /// is used by HTTP server to generate responses to commands.
+ ///
+ /// @return Pointer to the @ref CmdResponseCreator object.
+ virtual http::HttpResponseCreatorPtr create() const {
+ return (sole_creator_);
+ }
+
+private:
+
+ /// @brief Instance of the @ref CmdResponseCreator returned.
+ http::HttpResponseCreatorPtr sole_creator_;
+};
+
+} // end of namespace isc::config
+} // end of namespace isc
+
+#endif
extern const isc::log::MessageID COMMAND_ACCEPTOR_START = "COMMAND_ACCEPTOR_START";
extern const isc::log::MessageID COMMAND_DEREGISTERED = "COMMAND_DEREGISTERED";
extern const isc::log::MessageID COMMAND_EXTENDED_REGISTERED = "COMMAND_EXTENDED_REGISTERED";
+extern const isc::log::MessageID COMMAND_HTTP_LISTENER_STARTED = "COMMAND_HTTP_LISTENER_STARTED";
+extern const isc::log::MessageID COMMAND_HTTP_LISTENER_STOPPED = "COMMAND_HTTP_LISTENER_STOPPED";
+extern const isc::log::MessageID COMMAND_HTTP_LISTENER_STOPPING = "COMMAND_HTTP_LISTENER_STOPPING";
extern const isc::log::MessageID COMMAND_PROCESS_ERROR1 = "COMMAND_PROCESS_ERROR1";
extern const isc::log::MessageID COMMAND_PROCESS_ERROR2 = "COMMAND_PROCESS_ERROR2";
extern const isc::log::MessageID COMMAND_RECEIVED = "COMMAND_RECEIVED";
"COMMAND_ACCEPTOR_START", "Starting to accept connections via unix domain socket bound to %1",
"COMMAND_DEREGISTERED", "Command %1 deregistered",
"COMMAND_EXTENDED_REGISTERED", "Command %1 registered",
+ "COMMAND_HTTP_LISTENER_STARTED", "Command HTTP listener started with %1 threads, listening on %2:%3",
+ "COMMAND_HTTP_LISTENER_STOPPED", "Command HTTP listener for %1:%2 stopped.",
+ "COMMAND_HTTP_LISTENER_STOPPING", "Stopping Command HTTP listener for %1:%2",
"COMMAND_PROCESS_ERROR1", "Error while processing command: %1",
"COMMAND_PROCESS_ERROR2", "Error while processing command: %1",
"COMMAND_RECEIVED", "Received command '%1'",
extern const isc::log::MessageID COMMAND_ACCEPTOR_START;
extern const isc::log::MessageID COMMAND_DEREGISTERED;
extern const isc::log::MessageID COMMAND_EXTENDED_REGISTERED;
+extern const isc::log::MessageID COMMAND_HTTP_LISTENER_STARTED;
+extern const isc::log::MessageID COMMAND_HTTP_LISTENER_STOPPED;
+extern const isc::log::MessageID COMMAND_HTTP_LISTENER_STOPPING;
extern const isc::log::MessageID COMMAND_PROCESS_ERROR1;
extern const isc::log::MessageID COMMAND_PROCESS_ERROR2;
extern const isc::log::MessageID COMMAND_RECEIVED;
ready status after scheduling asynchronous send. This is programmatic error
that should be reported. The command manager may or may not continue
to operate correctly.
+
+% COMMAND_HTTP_LISTENER_STARTED Command HTTP listener started with %1 threads, listening on %2:%3
+This debug messages is issued when an HTTP listener has been started to
+accept connections from Command API clients through which commands can be
+received and responses sent. Arguments detail the number of threads
+that the listener is using, and the address and port at which it
+is listening.
+
+% COMMAND_HTTP_LISTENER_STOPPING Stopping Command HTTP listener for %1:%2
+This debug messages is issued when the Command HTTP listener, listening
+at the given address and port, has begun to shutdown.
+
+% COMMAND_HTTP_LISTENER_STOPPED Command HTTP listener for %1:%2 stopped.
+This debug messages is issued when the Command HTTP listener, listening
+at the given address and port, has completed shutdown.
+
run_unittests_SOURCES = client_connection_unittests.cc
run_unittests_SOURCES += run_unittests.cc
run_unittests_SOURCES += command_mgr_unittests.cc
+run_unittests_SOURCES += cmd_http_listener_unittests.cc
+run_unittests_SOURCES += cmd_response_creator_unittests.cc
+run_unittests_SOURCES += cmd_response_creator_factory_unittests.cc
run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
run_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
run_unittests_LDADD = $(top_builddir)/src/lib/asiolink/testutils/libasiolinktest.la
+run_unittests_LDADD += $(top_builddir)/src/lib/http/libkea-http.la
run_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
run_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
--- /dev/null
+// Copyright (C) 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/.
+
+#include <config.h>
+
+#include <config/cmd_http_listener.h>
+#include <testutils/gtest_utils.h>
+
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::config;
+using namespace isc::data;
+
+namespace {
+
+/// Verifies the construction, starting, stopping, destruction
+/// the CmdHttpListener class.
+TEST(CmdHttpListener, basics) {
+ CmdHttpListenerPtr listener;
+ asiolink::IOAddress address("127.0.0.1");
+ uint16_t port = 8080;
+
+ // Make sure we can create one.
+ ASSERT_NO_THROW_LOG(listener.reset(new CmdHttpListener(address, port)));
+ ASSERT_TRUE(listener);
+
+ // Verify the getters get what we expect.
+ EXPECT_EQ(listener->getAddress(), address);
+ EXPECT_EQ(listener->getPort(), port);
+ EXPECT_EQ(listener->getThreadPoolSize(), 1);
+
+ // It should not be listening and have no threads.
+ EXPECT_FALSE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 0);
+
+ // 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(), 1);
+
+ // Trying to start it again should fail.
+ ASSERT_THROW_MSG(listener->start(), InvalidOperation,
+ "CmdHttpListener is already listening!");
+
+ // Stop it and verify we're no longer listening.
+ ASSERT_NO_THROW_LOG(listener->stop());
+ ASSERT_FALSE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 0);
+
+ // Make sure we can call stop again without problems.
+ ASSERT_NO_THROW_LOG(listener->stop());
+
+ // We should be able to restart it.
+ ASSERT_NO_THROW_LOG(listener->start());
+ ASSERT_TRUE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 1);
+
+ // Destroying it should also stop it.
+ // If the test timeouts we know it didn't!
+ ASSERT_NO_THROW_LOG(listener.reset());
+
+ // Verify we can construct with more than one thread.
+ ASSERT_NO_THROW_LOG(listener.reset(new CmdHttpListener(address, port, 4)));
+ ASSERT_NO_THROW_LOG(listener->start());
+ EXPECT_EQ(listener->getAddress(), address);
+ EXPECT_EQ(listener->getPort(), port);
+ EXPECT_EQ(listener->getThreadPoolSize(), 4);
+ ASSERT_TRUE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 4);
+
+ // Stop it and verify we're no longer listening.
+ ASSERT_NO_THROW_LOG(listener->stop());
+ ASSERT_FALSE(listener->isListening());
+ EXPECT_EQ(listener->getThreadCount(), 0);
+}
+
+} // end of anonymous namespace
--- /dev/null
+// Copyright (C) 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/.
+
+#include <config.h>
+
+#include <config/cmd_response_creator_factory.h>
+#include <boost/pointer_cast.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace isc::config;
+
+namespace {
+
+// This test verifies the default factory constructor and
+// the create() method.
+TEST(CmdResponseCreatorFactory, createDefault) {
+ // Create the factory.
+ CmdResponseCreatorFactory factory;
+
+ // Create a response creator.
+ CmdResponseCreatorPtr response1;
+ ASSERT_NO_THROW(response1 = boost::dynamic_pointer_cast<
+ CmdResponseCreator>(factory.create()));
+ ASSERT_TRUE(response1);
+
+ // Agent response emulation should be enabled by default.
+ EXPECT_TRUE(response1->emulateAgentResponse());
+
+ // Authorization configuration should be an empty pointer.
+ EXPECT_FALSE(response1->getHttpAuthConfig());
+
+ // Invoke create() again.
+ CmdResponseCreatorPtr response2;
+ ASSERT_NO_THROW(response2 = boost::dynamic_pointer_cast<
+ CmdResponseCreator>(factory.create()));
+ ASSERT_TRUE(response2);
+
+ // And it must always return the same object.
+ EXPECT_TRUE(response1 == response2);
+}
+
+// This test verifies that agent response emulation can
+// be turned off.
+TEST(CmdResponseCreatorFactory, createAgentEmulationDisabled) {
+ // Instantiate the factory with agent emulation disabled.
+ CmdResponseCreatorFactory factory(false);
+
+ // Create the response creator.
+ CmdResponseCreatorPtr response;
+ ASSERT_NO_THROW(response = boost::dynamic_pointer_cast<
+ CmdResponseCreator>(factory.create()));
+ ASSERT_TRUE(response);
+
+ // Agent response emulation should be disabled.
+ EXPECT_FALSE(response->emulateAgentResponse());
+
+ // Authorization configuration should be an empty pointer.
+ EXPECT_FALSE(response->getHttpAuthConfig());
+}
+
+} // end of anonymous namespace
--- /dev/null
+// Copyright (C) 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/.
+
+#include <config.h>
+
+#include <cc/command_interpreter.h>
+#include <config/command_mgr.h>
+#include <config/cmd_response_creator.h>
+#include <http/post_request.h>
+#include <http/post_request_json.h>
+#include <http/response_json.h>
+
+#include <gtest/gtest.h>
+#include <boost/pointer_cast.hpp>
+#include <functional>
+
+using namespace isc;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::http;
+namespace ph = std::placeholders;
+
+namespace {
+
+/// @brief Test fixture class for @ref CmdResponseCreator.
+class CmdResponseCreatorTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Creates instance of the response creator and uses this instance to
+ /// create "empty" request. It also removes registered commands from the
+ /// command manager.
+ CmdResponseCreatorTest() {
+ // Deregisters commands.
+ config::CommandMgr::instance().deregisterAll();
+ // Register our "foo" command.
+ config::CommandMgr::instance().
+ registerCommand("foo", std::bind(&CmdResponseCreatorTest::
+ fooCommandHandler,
+ this, ph::_1, ph::_2));
+ }
+
+ /// @brief SetUp function that wraps call to initCreator.
+ /// Creates a default CmdResponseCreator and new HttpRequest.
+ virtual void SetUp() {
+ initCreator();
+ }
+
+ /// @brief Creates a new CmdResponseCreator and new HttpRequest.
+ ///
+ /// @param emulate_agent_flag enables/disables agent response emulation
+ /// in the CmdResponsCreator.
+ void initCreator(bool emulate_agent_flag = true) {
+ response_creator_.reset(new CmdResponseCreator(emulate_agent_flag));
+ request_ = response_creator_->createNewHttpRequest();
+ ASSERT_TRUE(request_) << "initCreator failed to create request";
+ }
+
+ /// @brief Destructor.
+ ///
+ /// Removes registered commands from the command manager.
+ virtual ~CmdResponseCreatorTest() {
+ config::CommandMgr::instance().deregisterAll();
+ }
+
+ /// @brief Fills request context with required data to create new request.
+ ///
+ /// @param request Request which context should be configured.
+ void setBasicContext(const HttpRequestPtr& request) {
+ request->context()->method_ = "POST";
+ request->context()->http_version_major_ = 1;
+ request->context()->http_version_minor_ = 1;
+ request->context()->uri_ = "/foo";
+
+ // Content-Type
+ HttpHeaderContext content_type;
+ content_type.name_ = "Content-Type";
+ content_type.value_ = "application/json";
+ request->context()->headers_.push_back(content_type);
+
+ // Content-Length
+ HttpHeaderContext content_length;
+ content_length.name_ = "Content-Length";
+ content_length.value_ = "0";
+ request->context()->headers_.push_back(content_length);
+ }
+
+ /// @brief Test creation of stock response.
+ ///
+ /// @param status_code Status code to be included in the response.
+ /// @param must_contain Text that must be present in the textual
+ /// representation of the generated response.
+ void testStockResponse(const HttpStatusCode& status_code,
+ const std::string& must_contain) {
+ HttpResponsePtr response;
+ ASSERT_NO_THROW(
+ response = response_creator_->createStockHttpResponse(request_,
+ status_code)
+ );
+ ASSERT_TRUE(response);
+ HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
+ HttpResponseJson>(response);
+ ASSERT_TRUE(response_json);
+ // Make sure the response contains the string specified as argument.
+ EXPECT_TRUE(response_json->toString().find(must_contain) != std::string::npos);
+
+ }
+
+ /// @brief Handler for the 'foo' test command.
+ ///
+ /// @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 Instance of the response creator.
+ CmdResponseCreatorPtr response_creator_;
+
+ /// @brief Instance of the "empty" request.
+ ///
+ /// The context belonging to this request may be modified by the unit
+ /// tests to verify various scenarios of response creation.
+ HttpRequestPtr request_;
+};
+
+// This test verifies that the created "empty" request has valid type.
+TEST_F(CmdResponseCreatorTest, createNewHttpRequest) {
+ // The request must be of PostHttpRequestJson type.
+ PostHttpRequestJsonPtr request_json = boost::dynamic_pointer_cast<
+ PostHttpRequestJson>(request_);
+ ASSERT_TRUE(request_json);
+}
+
+// Test that HTTP version of stock response is set to 1.0 if the request
+// context doesn't specify any version.
+TEST_F(CmdResponseCreatorTest, createStockHttpResponseNoVersion) {
+ testStockResponse(HttpStatusCode::BAD_REQUEST, "HTTP/1.0 400 Bad Request");
+}
+
+// Test that HTTP version of stock response is set to 1.0 if the request
+// version is higher than 1.1.
+TEST_F(CmdResponseCreatorTest, createStockHttpResponseHighVersion) {
+ request_->context()->http_version_major_ = 2;
+ testStockResponse(HttpStatusCode::REQUEST_TIMEOUT,
+ "HTTP/1.0 408 Request Timeout");
+}
+
+// Test that the server responds with version 1.1 if request version is 1.1.
+TEST_F(CmdResponseCreatorTest, createStockHttpResponseCorrectVersion) {
+ request_->context()->http_version_major_ = 1;
+ request_->context()->http_version_minor_ = 1;
+ testStockResponse(HttpStatusCode::NO_CONTENT, "HTTP/1.1 204 No Content");
+}
+
+// Test successful server response when the client specifies valid command.
+TEST_F(CmdResponseCreatorTest, createDynamicHttpResponse) {
+ setBasicContext(request_);
+
+ // Body: "foo" command has been registered in the test fixture constructor.
+ request_->context()->body_ = "{ \"command\": \"foo\" }";
+
+ // All requests must be finalized before they can be processed.
+ ASSERT_NO_THROW(request_->finalize());
+
+ // Create response from the request.
+ HttpResponsePtr response;
+ ASSERT_NO_THROW(response = response_creator_->createHttpResponse(request_));
+ ASSERT_TRUE(response);
+
+ // Response must be convertible to HttpResponseJsonPtr.
+ HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
+ HttpResponseJson>(response);
+ ASSERT_TRUE(response_json);
+
+ // Response should be in a list by default.
+ ASSERT_TRUE(response_creator_->emulateAgentResponse());
+ ASSERT_TRUE(response_json->getBodyAsJson()->getType() == Element::list)
+ << "response is not a list: " << response_json->toString();
+
+ // Response must be successful.
+ EXPECT_TRUE(response_json->toString().find("HTTP/1.1 200 OK") !=
+ std::string::npos);
+
+ // Response must contain JSON body with "result" of 0.
+ EXPECT_TRUE(response_json->toString().find("\"result\": 0") !=
+ std::string::npos);
+}
+
+// Test successful server response without emulating agent response.
+TEST_F(CmdResponseCreatorTest, createDynamicHttpResponseNoEmulation) {
+ // Recreate the response creator setting emulate_agent_response to false;
+ initCreator(false);
+ setBasicContext(request_);
+
+ // Body: "foo" command has been registered in the test fixture constructor.
+ request_->context()->body_ = "{ \"command\": \"foo\" }";
+
+ // All requests must be finalized before they can be processed.
+ ASSERT_NO_THROW(request_->finalize());
+
+ // Create response from the request.
+ HttpResponsePtr response;
+ ASSERT_NO_THROW(response = response_creator_->createHttpResponse(request_));
+ ASSERT_TRUE(response);
+
+ // Response must be convertible to HttpResponseJsonPtr.
+ HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
+ HttpResponseJson>(response);
+ ASSERT_TRUE(response_json);
+
+ // Response should be a map that is not enclosed in a list.
+ ASSERT_FALSE(response_creator_->emulateAgentResponse());
+ ASSERT_TRUE(response_json->getBodyAsJson()->getType() == Element::map)
+ << "response is not a list: " << response_json->toString();
+
+ // Response must be successful.
+ EXPECT_TRUE(response_json->toString().find("HTTP/1.1 200 OK") !=
+ std::string::npos);
+
+ // Response must contain JSON body with "result" of 0.
+ EXPECT_TRUE(response_json->toString().find("\"result\": 0") !=
+ std::string::npos);
+}
+
+
+// This test verifies that Internal Server Error is returned when invalid C++
+// request type is used. This is considered an error in the server logic.
+TEST_F(CmdResponseCreatorTest, createDynamicHttpResponseInvalidType) {
+ PostHttpRequestPtr request(new PostHttpRequest());
+ setBasicContext(request);
+
+ // Body: "list-commands" is natively supported by the command manager.
+ request->context()->body_ = "{ \"command\": \"list-commands\" }";
+
+ // All requests must be finalized before they can be processed.
+ ASSERT_NO_THROW(request->finalize());
+
+ HttpResponsePtr response;
+ ASSERT_NO_THROW(response = response_creator_->createHttpResponse(request));
+ ASSERT_TRUE(response);
+
+ // Response must be convertible to HttpResponseJsonPtr.
+ HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
+ HttpResponseJson>(response);
+ ASSERT_TRUE(response_json);
+
+ // Response must contain Internal Server Error status code.
+ EXPECT_TRUE(response_json->toString().find("HTTP/1.1 500 Internal Server Error") !=
+ std::string::npos);
+}
+
+}