]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1730] CmdHttpListener - initial implementation
authorThomas Markwalder <tmark@isc.org>
Thu, 4 Mar 2021 15:50:50 +0000 (10:50 -0500)
committerThomas Markwalder <tmark@isc.org>
Mon, 22 Mar 2021 17:51:50 +0000 (13:51 -0400)
Initial implementation is complete.  Note class is
not used anywhere but unit tests.

Need unit tests to verify mulitple client connections
and "command" processing.

New files:
src/lib/config/cmd_http_listener.cc
src/lib/config/cmd_http_listener.h
src/lib/config/cmd_response_creator.cc
src/lib/config/cmd_response_creator.h
src/lib/config/cmd_response_creator_factory.h

src/lib/config/Makefile.am
    added new files

src/lib/config/config_messages.mes
src/lib/config/config_messages.cc
src/lib/config/config_messages.h
    - new messages

New files:
src/lib/config/tests/cmd_http_listener_unittests.cc
src/lib/config/tests/cmd_response_creator_unittests.cc
src/lib/config/tests/cmd_response_creator_factory_unittests.cc

src/lib/config/tests/Makefile.am
    added new files

13 files changed:
src/lib/config/Makefile.am
src/lib/config/cmd_http_listener.cc [new file with mode: 0644]
src/lib/config/cmd_http_listener.h [new file with mode: 0644]
src/lib/config/cmd_response_creator.cc [new file with mode: 0644]
src/lib/config/cmd_response_creator.h [new file with mode: 0644]
src/lib/config/cmd_response_creator_factory.h [new file with mode: 0644]
src/lib/config/config_messages.cc
src/lib/config/config_messages.h
src/lib/config/config_messages.mes
src/lib/config/tests/Makefile.am
src/lib/config/tests/cmd_http_listener_unittests.cc [new file with mode: 0644]
src/lib/config/tests/cmd_response_creator_factory_unittests.cc [new file with mode: 0644]
src/lib/config/tests/cmd_response_creator_unittests.cc [new file with mode: 0644]

index f97d7760bf57460a3b46a4035b8616cdcdbab38a..dc4e4bc0473676d1e44d7b4836421074ef2688f0 100644 (file)
@@ -14,8 +14,12 @@ libkea_cfgclient_la_SOURCES += config_log.h config_log.cc
 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
diff --git a/src/lib/config/cmd_http_listener.cc b/src/lib/config/cmd_http_listener.cc
new file mode 100644 (file)
index 0000000..31d1a22
--- /dev/null
@@ -0,0 +1,118 @@
+// 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
diff --git a/src/lib/config/cmd_http_listener.h b/src/lib/config/cmd_http_listener.h
new file mode 100644 (file)
index 0000000..ce3b2a5
--- /dev/null
@@ -0,0 +1,93 @@
+// 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
diff --git a/src/lib/config/cmd_response_creator.cc b/src/lib/config/cmd_response_creator.cc
new file mode 100644 (file)
index 0000000..62ebe6f
--- /dev/null
@@ -0,0 +1,119 @@
+// 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
diff --git a/src/lib/config/cmd_response_creator.h b/src/lib/config/cmd_response_creator.h
new file mode 100644 (file)
index 0000000..2a6d897
--- /dev/null
@@ -0,0 +1,117 @@
+// 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
diff --git a/src/lib/config/cmd_response_creator_factory.h b/src/lib/config/cmd_response_creator_factory.h
new file mode 100644 (file)
index 0000000..4b25f47
--- /dev/null
@@ -0,0 +1,63 @@
+// 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
index ea983c933891a2399484a3acbd57f1cb54dd7454..e4d7ce3825245ae763bb75aa7d5ffb0f9abbfd21 100644 (file)
@@ -10,6 +10,9 @@ namespace config {
 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";
@@ -40,6 +43,9 @@ const char* values[] = {
     "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'",
index 9ab2e64377f120265b74d6560f1311cb879f5558..06a88f5bb06ea3776a9bd207edaccd4b2dd2f1ec 100644 (file)
@@ -11,6 +11,9 @@ namespace config {
 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;
index 66dd1531152f0057839072e148ed7e7253fb52a6..653e53cb3f19466bbf7356b0d53239267e04a7c5 100644 (file)
@@ -122,3 +122,19 @@ This error message is issued when the command manager was unable to set
 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.
+
index 47a918f556fb46018d0058937f72924c6c3fc8e1..e27415913c65c524691e894a3d901fd7c033763c 100644 (file)
@@ -22,11 +22,15 @@ TESTS += run_unittests
 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
diff --git a/src/lib/config/tests/cmd_http_listener_unittests.cc b/src/lib/config/tests/cmd_http_listener_unittests.cc
new file mode 100644 (file)
index 0000000..b2d6dc2
--- /dev/null
@@ -0,0 +1,81 @@
+// 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
diff --git a/src/lib/config/tests/cmd_response_creator_factory_unittests.cc b/src/lib/config/tests/cmd_response_creator_factory_unittests.cc
new file mode 100644 (file)
index 0000000..3ee28cb
--- /dev/null
@@ -0,0 +1,65 @@
+// 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
diff --git a/src/lib/config/tests/cmd_response_creator_unittests.cc b/src/lib/config/tests/cmd_response_creator_unittests.cc
new file mode 100644 (file)
index 0000000..125da71
--- /dev/null
@@ -0,0 +1,262 @@
+// 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);
+}
+
+}