]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3506] Checkpoint: split UnixCommandMgr
authorFrancis Dupont <fdupont@isc.org>
Sun, 1 Sep 2024 18:55:56 +0000 (20:55 +0200)
committerFrancis Dupont <fdupont@isc.org>
Fri, 20 Sep 2024 12:55:54 +0000 (14:55 +0200)
25 files changed:
src/bin/agent/ca_cfg_mgr.h
src/bin/d2/d2_controller.cc
src/bin/d2/d2_process.cc
src/bin/d2/tests/d2_command_unittest.cc
src/bin/dhcp4/ctrl_dhcp4_srv.cc
src/bin/dhcp4/json_config_parser.cc
src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
src/bin/dhcp4/tests/dhcp4_test_utils.h
src/bin/dhcp6/ctrl_dhcp6_srv.cc
src/bin/dhcp6/json_config_parser.cc
src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc
src/bin/dhcp6/tests/dhcp6_test_utils.h
src/lib/config/Makefile.am
src/lib/config/command-socket.dox
src/lib/config/command_mgr.cc
src/lib/config/command_mgr.h
src/lib/config/http_command_mgr.cc
src/lib/config/http_command_mgr.h
src/lib/config/tests/Makefile.am
src/lib/config/tests/command_mgr_unittests.cc
src/lib/config/tests/unix_command_mgr_unittests.cc [new file with mode: 0644]
src/lib/config/unix_command_mgr.cc [new file with mode: 0644]
src/lib/config/unix_command_mgr.h [new file with mode: 0644]

index 6234bd44f66f415609ef78326ee47f2f59e4943a..1cbfa82837e83c3e50d9f4c5eca6d94c9fd5364b 100644 (file)
@@ -50,7 +50,7 @@ public:
     /// This method returns Element tree structure that describes the control
     /// socket (or null pointer if the socket is not defined for a particular
     /// server type). This information is expected to be compatible with
-    /// data passed to @ref isc::config::CommandMgr::openCommandSocket.
+    /// data passed to @ref isc::config::UnixCommandMgr::openCommandSocket.
     ///
     /// @param service server being controlled
     /// @return pointer to the Element that holds control-socket map (or NULL)
@@ -61,7 +61,7 @@ public:
     ///
     /// This method stores Element tree structure that describes the control
     /// socket. This information is expected to be compatible with
-    /// data passed to @ref isc::config::CommandMgr::openCommandSocket.
+    /// data passed to @ref isc::config::UnixCommandMgr::openCommandSocket.
     ///
     /// @param control_socket Element that holds control-socket map
     /// @param service server being controlled
index 9534d0b9bff26ad16119ed23297ab144ec5f953a..0dfd021d2d6054ff4ff0ad200c16e98a02585707 100644 (file)
@@ -8,6 +8,7 @@
 
 #include <config/command_mgr.h>
 #include <config/http_command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <d2/d2_controller.h>
 #include <d2/d2_process.h>
 #include <d2/parser_context.h>
@@ -105,7 +106,7 @@ void
 D2Controller::deregisterCommands() {
     try {
         // Close command sockets.
-        CommandMgr::instance().closeCommandSocket();
+        UnixCommandMgr::instance().closeCommandSocket();
         HttpCommandMgr::instance().close();
 
         // Deregister any registered commands (please keep in alphabetic order)
index 2974e8fb2ab6a3a6dac33f05cff3352958393e19..8b0da4cf2d38050cf28a9de6c74fd0110bb1ee35 100644 (file)
@@ -10,6 +10,7 @@
 #include <cc/command_interpreter.h>
 #include <config/command_mgr.h>
 #include <config/http_command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <d2/d2_controller.h>
 #include <d2/d2_process.h>
 #include <d2srv/d2_cfg_mgr.h>
@@ -77,7 +78,7 @@ void
 D2Process::init() {
     using namespace isc::config;
     // Command managers use IO service to run asynchronous socket operations.
-    CommandMgr::instance().setIOService(getIOService());
+    UnixCommandMgr::instance().setIOService(getIOService());
     HttpCommandMgr::instance().setIOService(getIOService());
 
     // Set the HTTP authentication default realm.
@@ -161,7 +162,7 @@ D2Process::runIO() {
         // service is stopped it will return immediately with a cnt of zero.
         cnt = getIOService()->runOne();
     }
-    config::HttpCommandMgr::instance().garbageCollectListeners();
+    HttpCommandMgr::instance().garbageCollectListeners();
     return (cnt);
 }
 
@@ -515,13 +516,13 @@ D2Process::reconfigureCommandChannel() {
     if (!sock_cfg || !current_control_socket_ || sock_changed) {
         // Close the existing socket.
         if (current_control_socket_) {
-            isc::config::CommandMgr::instance().closeCommandSocket();
+            UnixCommandMgr::instance().closeCommandSocket();
             current_control_socket_.reset();
         }
 
         // Open the new socket.
         if (sock_cfg) {
-            isc::config::CommandMgr::instance().openCommandSocket(sock_cfg);
+            UnixCommandMgr::instance().openCommandSocket(sock_cfg);
         }
     }
 
index f9f76049270b113228d878d4ed3b96ff2406df61..a63a505ce7e253457aec3536ec1f35f0ca7808fe 100644 (file)
@@ -11,6 +11,7 @@
 #include <cc/command_interpreter.h>
 #include <config/command_mgr.h>
 #include <config/timeouts.h>
+#include <config/unix_command_mgr.h>
 #include <testutils/io_utils.h>
 #include <testutils/unix_control_client.h>
 #include <d2/d2_controller.h>
@@ -144,7 +145,7 @@ public:
 
         // Reset command manager.
         CommandMgr::instance().deregisterAll();
-        CommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND);
+        UnixCommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND);
     }
 
     /// @brief Returns pointer to the server's IO service.
@@ -216,7 +217,7 @@ public:
         ASSERT_EQ(0, status) << txt->str();
 
         // Now check that the socket was indeed open.
-        ASSERT_GT(CommandMgr::instance().getControlSocketFD(), -1);
+        ASSERT_GT(UnixCommandMgr::instance().getControlSocketFD(), -1);
     }
 
     /// @brief Conducts a command/response exchange via UnixCommandSocket.
@@ -456,7 +457,7 @@ TEST_F(CtrlChannelD2Test, configure) {
     ASSERT_EQ(Element::string, txt->getType());
     EXPECT_EQ("'socket-type' parameter is mandatory in control-sockets items",
               txt->stringValue());
-    EXPECT_EQ(-1, CommandMgr::instance().getControlSocketFD());
+    EXPECT_EQ(-1, UnixCommandMgr::instance().getControlSocketFD());
 
     // no name.
     string bad3 =
@@ -482,7 +483,7 @@ TEST_F(CtrlChannelD2Test, configure) {
     ASSERT_EQ(Element::string, txt->getType());
     EXPECT_EQ("Mandatory 'socket-name' parameter missing",
               txt->stringValue());
-    EXPECT_EQ(-1, CommandMgr::instance().getControlSocketFD());
+    EXPECT_EQ(-1, UnixCommandMgr::instance().getControlSocketFD());
 }
 
 // This test checks which commands are registered by the D2 server.
@@ -787,7 +788,7 @@ TEST_F(CtrlChannelD2Test, configTest) {
     ASSERT_TRUE(keys);
     EXPECT_EQ(1, keys->size());
 
-    ASSERT_GT(CommandMgr::instance().getControlSocketFD(), -1);
+    ASSERT_GT(UnixCommandMgr::instance().getControlSocketFD(), -1);
 
     // Create a config with invalid content that should fail to parse.
     os.str("");
@@ -926,7 +927,7 @@ TEST_F(CtrlChannelD2Test, configSet) {
     ASSERT_TRUE(keys);
     EXPECT_EQ(1, keys->size());
 
-    ASSERT_GT(CommandMgr::instance().getControlSocketFD(), -1);
+    ASSERT_GT(UnixCommandMgr::instance().getControlSocketFD(), -1);
 
     // Create a config with invalid content that should fail to parse.
     os.str("");
@@ -1316,7 +1317,7 @@ TEST_F(CtrlChannelD2Test, connectionTimeoutPartialCommand) {
     // Set connection timeout to 2s to prevent long waiting time for the
     // timeout during this test.
     const unsigned short timeout = 2000;
-    CommandMgr::instance().setConnectionTimeout(timeout);
+    UnixCommandMgr::instance().setConnectionTimeout(timeout);
 
     // Server's response will be assigned to this variable.
     string response;
@@ -1369,7 +1370,7 @@ TEST_F(CtrlChannelD2Test, connectionTimeoutNoData) {
     // Set connection timeout to 2s to prevent long waiting time for the
     // timeout during this test.
     const unsigned short timeout = 2000;
-    CommandMgr::instance().setConnectionTimeout(timeout);
+    UnixCommandMgr::instance().setConnectionTimeout(timeout);
 
     // Server's response will be assigned to this variable.
     string response;
index 4201b60cb70de0be7fad41a7529eab7f7a5b6de6..f464e0a94c6233349a579cf665d74d2f769c4f26 100644 (file)
@@ -12,6 +12,7 @@
 #include <cc/data.h>
 #include <config/command_mgr.h>
 #include <config/http_command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <cryptolink/crypto_hash.h>
 #include <dhcp/libdhcp++.h>
 #include <dhcp4/ctrl_dhcp4_srv.h>
@@ -1082,7 +1083,7 @@ ControlledDhcpv4Srv::ControlledDhcpv4Srv(uint16_t server_port /*= DHCP4_SERVER_P
     TimerMgr::instance()->setIOService(getIOService());
 
     // Command managers use IO service to run asynchronous socket operations.
-    CommandMgr::instance().setIOService(getIOService());
+    UnixCommandMgr::instance().setIOService(getIOService());
     HttpCommandMgr::instance().setIOService(getIOService());
 
     // Set the HTTP authentication default realm.
@@ -1193,7 +1194,7 @@ ControlledDhcpv4Srv::~ControlledDhcpv4Srv() {
         cleanup();
 
         // Close command sockets.
-        CommandMgr::instance().closeCommandSocket();
+        UnixCommandMgr::instance().closeCommandSocket();
         HttpCommandMgr::instance().close();
 
         // Deregister any registered commands (please keep in alphabetic order)
index 79dec5174a289e5bf1bfa969bbda29e97958b595..a24408341394f48dd364d183502519b5ca4df76a 100644 (file)
@@ -10,6 +10,7 @@
 #include <cc/command_interpreter.h>
 #include <config/command_mgr.h>
 #include <config/http_command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <database/database_connection.h>
 #include <database/dbaccess_parser.h>
 #include <database/backend_selector.h>
@@ -340,14 +341,14 @@ void configureCommandChannel() {
     // receive the configuration result.
     if (!sock_cfg || !current_sock_cfg || sock_changed) {
         // Close the existing socket (if any).
-        CommandMgr::instance().closeCommandSocket();
+        UnixCommandMgr::instance().closeCommandSocket();
 
         if (sock_cfg) {
             // This will create a control socket and install the external
             // socket in IfaceMgr. That socket will be monitored when
             // Dhcp4Srv::receivePacket() calls IfaceMgr::receive4() and
             // callback in CommandMgr will be called, if necessary.
-            CommandMgr::instance().openCommandSocket(sock_cfg);
+            UnixCommandMgr::instance().openCommandSocket(sock_cfg);
         }
     }
 
index 169e6aa2e40320324f5e12646ec965330c5871e0..632e1df759d5d82e7e9c64760dfe59e21be1d262 100644 (file)
@@ -10,6 +10,7 @@
 #include <asiolink/io_service.h>
 #include <cc/command_interpreter.h>
 #include <config/command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <config/timeouts.h>
 #include <dhcp/dhcp4.h>
 #include <dhcp/libdhcp++.h>
@@ -131,9 +132,9 @@ public:
         LeaseMgrFactory::destroy();
         StatsMgr::instance().removeAll();
 
-        CommandMgr::instance().closeCommandSocket();
+        UnixCommandMgr::instance().closeCommandSocket();
         CommandMgr::instance().deregisterAll();
-        CommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND);
+        UnixCommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND);
 
         server_.reset();
         reset();
@@ -222,7 +223,7 @@ public:
         ASSERT_EQ(0, status) << txt->str();
 
         // Now check that the socket was indeed open.
-        ASSERT_GT(isc::config::CommandMgr::instance().getControlSocketFD(), -1);
+        ASSERT_GT(isc::config::UnixCommandMgr::instance().getControlSocketFD(), -1);
     }
 
     /// @brief Reset hooks data
@@ -2254,7 +2255,7 @@ TEST_F(CtrlChannelDhcpv4SrvTest, connectionTimeoutPartialCommand) {
     // Set connection timeout to 2s to prevent long waiting time for the
     // timeout during this test.
     const unsigned short timeout = 2000;
-    CommandMgr::instance().setConnectionTimeout(timeout);
+    UnixCommandMgr::instance().setConnectionTimeout(timeout);
 
     // Server's response will be assigned to this variable.
     std::string response;
@@ -2308,7 +2309,7 @@ TEST_F(CtrlChannelDhcpv4SrvTest, connectionTimeoutNoData) {
     // Set connection timeout to 2s to prevent long waiting time for the
     // timeout during this test.
     const unsigned short timeout = 2000;
-    CommandMgr::instance().setConnectionTimeout(timeout);
+    UnixCommandMgr::instance().setConnectionTimeout(timeout);
 
     // Server's response will be assigned to this variable.
     std::string response;
index 0b50668dd42f661da9cff55379cb596c06220e54..0dccec99148cae2ca8dbcac535c03520a7fba280 100644 (file)
@@ -8,7 +8,7 @@
 
 #include <asiolink/io_address.h>
 #include <cc/command_interpreter.h>
-#include <config/command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <config_backend/base_config_backend.h>
 #include <dhcp4/dhcp4_log.h>
 #include <dhcp4/dhcp4_srv.h>
@@ -2965,10 +2965,10 @@ Dhcpv4SrvTest::loadConfigFile(const string& path) {
     TimerMgr::instance()->unregisterTimers();
 
     // Close the command socket (if it exists).
-    CommandMgr::instance().closeCommandSocket();
+    UnixCommandMgr::instance().closeCommandSocket();
 
     // Reset CommandMgr IO service.
-    CommandMgr::instance().setIOService(IOServicePtr());
+    UnixCommandMgr::instance().setIOService(IOServicePtr());
 
     // Reset DatabaseConnection IO service.
     DatabaseConnection::setIOService(IOServicePtr());
index 269e6906f01eaf9fbf51bfac18a19823714a1e4c..9988e3631cbf70e51acc2377dea59cdf331c60dd 100644 (file)
@@ -28,6 +28,7 @@
 #include <asiolink/io_address.h>
 #include <cc/command_interpreter.h>
 #include <config/command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <util/multi_threading_mgr.h>
 #include <list>
 
@@ -136,7 +137,7 @@ public:
 
         dhcp::TimerMgr::instance()->setIOService(getIOService());
 
-        config::CommandMgr::instance().setIOService(getIOService());
+        config::UnixCommandMgr::instance().setIOService(getIOService());
     }
 
     /// @brief Returns fixed server identifier assigned to the naked server
index 43eb8392c8e013776b198726a1713faf4856bfb5..635212eda50c09752b0dfe10b5d476d5ff03e61a 100644 (file)
@@ -12,6 +12,7 @@
 #include <cc/data.h>
 #include <config/command_mgr.h>
 #include <config/http_command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <cryptolink/crypto_hash.h>
 #include <dhcp/libdhcp++.h>
 #include <dhcp6/ctrl_dhcp6_srv.h>
@@ -1106,7 +1107,7 @@ ControlledDhcpv6Srv::ControlledDhcpv6Srv(uint16_t server_port /*= DHCP6_SERVER_P
     TimerMgr::instance()->setIOService(getIOService());
 
     // Command managers use IO service to run asynchronous socket operations.
-    CommandMgr::instance().setIOService(getIOService());
+    UnixCommandMgr::instance().setIOService(getIOService());
     HttpCommandMgr::instance().setIOService(getIOService());
 
     // Set the HTTP default socket address to the IPv6 (vs IPv4) loopback.
@@ -1220,7 +1221,7 @@ ControlledDhcpv6Srv::~ControlledDhcpv6Srv() {
         cleanup();
 
         // Close command sockets.
-        CommandMgr::instance().closeCommandSocket();
+        UnixCommandMgr::instance().closeCommandSocket();
         HttpCommandMgr::instance().close();
 
         // Deregister any registered commands (please keep in alphabetic order)
index b9d26a0a1e442afa4079cb103f75d0e50bc2b1cb..eb5923a5de1b4a5f8e994a45761b17380b9b8280 100644 (file)
@@ -12,6 +12,7 @@
 #include <cc/command_interpreter.h>
 #include <config/command_mgr.h>
 #include <config/http_command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <database/database_connection.h>
 #include <database/dbaccess_parser.h>
 #include <dhcp6/ctrl_dhcp6_srv.h>
@@ -442,14 +443,14 @@ void configureCommandChannel() {
     // receive the configuration result.
     if (!sock_cfg || !current_sock_cfg || sock_changed) {
         // Close the existing socket (if any).
-        CommandMgr::instance().closeCommandSocket();
+        UnixCommandMgr::instance().closeCommandSocket();
 
         if (sock_cfg) {
             // This will create a control socket and install the external
             // socket in IfaceMgr. That socket will be monitored when
             // Dhcp6Srv::receivePacket() calls IfaceMgr::receive6() and
             // callback in CommandMgr will be called, if necessary.
-            CommandMgr::instance().openCommandSocket(sock_cfg);
+            UnixCommandMgr::instance().openCommandSocket(sock_cfg);
         }
     }
 
index fc1271b070f41489a7cbf088fd3be84c60c70021..131bf179aadfb751df00b5c78af1982106ecc218 100644 (file)
@@ -10,6 +10,7 @@
 #include <cc/command_interpreter.h>
 #include <config/command_mgr.h>
 #include <config/timeouts.h>
+#include <config/unix_command_mgr.h>
 #include <dhcp/libdhcp++.h>
 #include <dhcp/testutils/iface_mgr_test_config.h>
 #include <dhcpsrv/cfgmgr.h>
@@ -111,7 +112,7 @@ public:
         LeaseMgrFactory::destroy();
         StatsMgr::instance().removeAll();
         CommandMgr::instance().deregisterAll();
-        CommandMgr::instance().setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT);
+        UnixCommandMgr::instance().setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT);
 
         reset();
     }
@@ -167,9 +168,9 @@ public:
         LeaseMgrFactory::destroy();
         StatsMgr::instance().removeAll();
 
-        CommandMgr::instance().closeCommandSocket();
+        UnixCommandMgr::instance().closeCommandSocket();
         CommandMgr::instance().deregisterAll();
-        CommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND);
+        UnixCommandMgr::instance().setConnectionTimeout(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND);
 
         server_.reset();
         reset();
@@ -258,7 +259,7 @@ public:
         ASSERT_EQ(0, status) << txt->str();
 
         // Now check that the socket was indeed open.
-        ASSERT_GT(isc::config::CommandMgr::instance().getControlSocketFD(), -1);
+        ASSERT_GT(isc::config::UnixCommandMgr::instance().getControlSocketFD(), -1);
     }
 
     /// @brief Reset
@@ -2289,7 +2290,7 @@ TEST_F(CtrlChannelDhcpv6SrvTest, connectionTimeoutPartialCommand) {
     // Set connection timeout to 2s to prevent long waiting time for the
     // timeout during this test.
     const unsigned short timeout = 2000;
-    CommandMgr::instance().setConnectionTimeout(timeout);
+    UnixCommandMgr::instance().setConnectionTimeout(timeout);
 
     // Server's response will be assigned to this variable.
     std::string response;
@@ -2343,7 +2344,7 @@ TEST_F(CtrlChannelDhcpv6SrvTest, connectionTimeoutNoData) {
     // Set connection timeout to 2s to prevent long waiting time for the
     // timeout during this test.
     const unsigned short timeout = 2000;
-    CommandMgr::instance().setConnectionTimeout(timeout);
+    UnixCommandMgr::instance().setConnectionTimeout(timeout);
 
     // Server's response will be assigned to this variable.
     std::string response;
index 0a3d1feefc5e9489427b5679141c2b7cd42b5bd0..d9648bf4ade14d10765e20b034f2656c21571ccc 100644 (file)
@@ -9,6 +9,7 @@
 #include <asiolink/io_address.h>
 #include <cc/command_interpreter.h>
 #include <config/command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <config_backend/base_config_backend.h>
 #include <dhcp6/json_config_parser.h>
 #include <dhcp6/tests/dhcp6_test_utils.h>
@@ -329,10 +330,10 @@ Dhcpv6SrvTest::loadConfigFile(const string& path) {
     TimerMgr::instance()->unregisterTimers();
 
     // Close the command socket (if it exists).
-    CommandMgr::instance().closeCommandSocket();
+    UnixCommandMgr::instance().closeCommandSocket();
 
     // Reset CommandMgr IO service.
-    CommandMgr::instance().setIOService(IOServicePtr());
+    UnixCommandMgr::instance().setIOService(IOServicePtr());
 
     // Reset DatabaseConnection IO service.
     DatabaseConnection::setIOService(IOServicePtr());
index fd7a11f474d5b0bd41c342fc8372dafc5ea4455d..b0e23488a142de231efbd3f66748cdc2a4712f67 100644 (file)
@@ -31,6 +31,7 @@
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <hooks/hooks_manager.h>
 #include <config/command_mgr.h>
+#include <config/unix_command_mgr.h>
 #include <util/multi_threading_mgr.h>
 #include <testutils/log_utils.h>
 
@@ -145,7 +146,7 @@ public:
 
         dhcp::TimerMgr::instance()->setIOService(getIOService());
 
-        config::CommandMgr::instance().setIOService(getIOService());
+        config::UnixCommandMgr::instance().setIOService(getIOService());
     }
 
     /// @brief fakes packet reception
index fab12367015b4f786110b0b1e13132b832892dc3..b31544365fad629c3cbc6a3cbbaf99f6de43f9aa 100644 (file)
@@ -10,6 +10,7 @@ libkea_cfgclient_la_SOURCES  = cmds_impl.h
 libkea_cfgclient_la_SOURCES += base_command_mgr.cc base_command_mgr.h
 libkea_cfgclient_la_SOURCES += client_connection.cc client_connection.h
 libkea_cfgclient_la_SOURCES += command_mgr.cc command_mgr.h
+libkea_cfgclient_la_SOURCES += unix_command_mgr.cc unix_command_mgr.h
 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
index 3b5150afa3bc37f5fb726aab143e1e5ba6b94cf0..7413746576587b1455a5b4dbf692dfc98ae33d92 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2015-2020 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2024 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
@@ -18,8 +18,7 @@ envisioned are: reconfiguration, statistics retrieval and manipulation,
 and shutdown.
 
 Communication over Control Channel is conducted using JSON structures.
-As of Kea 0.9.2, the only supported communication channel is UNIX stream
-socket, but additional types may be added in the future.
+The supported communication channel is UNIX stream and HTTP/HTTPS sockets.
 
 If configured, Kea will open a socket and will listen for any incoming
 connections. A process connecting to this socket is expected to send JSON
@@ -62,9 +61,9 @@ encountered.
 - arguments - is a map of additional data values returned by the server, specific to the
 command issue. The map is always present, even if it contains no data values.
 
-@section ctrlSocketClient Using Control Channel
+@section ctrlSocketClient Using UNIX Control Channel
 
-Here are two examples of how to access the Control Channel:
+Here are two examples of how to access the UNIX Control Channel:
 
 1. Use socat tool, which is available in many Linux and BSD distributions.
 See http://www.dest-unreach.org/socat/ for details. To use it:
@@ -147,30 +146,33 @@ There are 3 main methods that are expected to be used by developers:
 - @ref isc::config::CommandMgr::processCommand, which allows handling specified
   command.
 
-There are also two methods for managing control sockets. They are not expected
-to be used directly, unless someone implements a new Control Channel (e.g. TCP
-or HTTPS connection):
-
-- @ref isc::config::CommandMgr::openCommandSocket that passes structure defined
-  in the configuration file. Currently only two parameters are supported: socket-type
-  (which must contain value 'unix') and socket-name (which contains unix path for
-  the named socket to be created).
-- @ref isc::config::CommandMgr::closeCommandSocket() - it is used to close the
-  socket.
-
 Kea servers use @c CommandMgr to register handlers for various commands they
 support natively. However, it is possible extend a set of supported commands
 using hooks framework. See @ref hooksdgCommandHandlers how to implement support
 for your own control commands in Kea.
 
+@section unixCtrlSocketImpl UNIX Control Channel Implementation
+
+UNIX Control Channel is implemented in @ref isc::config::UnixCommandMgr.
+It is a singleton class providing two methods for managing control sockets.
+They are not expected to be used directly.
+
+- @ref isc::config::UnixCommandMgr::openCommandSocket that passes
+  structure defined in the configuration file. Currently only two
+  parameters are supported: socket-type (which must contain value
+  'unix') and socket-name (which contains unix path for the named
+  socket to be created).
+- @ref isc::config::UnixCommandMgr::closeCommandSocket() - it is used
+  to close the socket.
+
 @section ctrlSocketConnections Accepting connections
 
-The @ref isc::config::CommandMgr is implemented using boost ASIO and uses
+The @ref isc::config::UnixCommandMgr is implemented using boost ASIO and uses
 asynchronous calls to accept new connections and receive commands from the
 controlling clients. ASIO uses IO service object to run asynchronous calls.
-Thus, before the server can use the @ref isc::config::CommandMgr it must
+Thus, before the server can use the @ref isc::config::UnixCommandMgr it must
 provide it with a common instance of the @ref isc::asiolink::IOService
-object using @ref isc::config::CommandMgr::setIOService. The server's
+object using @ref isc::config::UnixCommandMgr::setIOService. The server's
 main loop must contain calls to @ref isc::asiolink::IOService::run or
 @ref isc::asiolink::IOService::poll or their variants to invoke Command
 Manager's handlers as required for processing control requests.
index cf21a697abc96742bd90296a0fb01f9feca45006..13a94e0f0cb4dbc49ca5b99c2f26a86f9bd01193 100644 (file)
 
 #include <config.h>
 
-#include <asiolink/asio_wrapper.h>
-#include <asiolink/interval_timer.h>
-#include <asiolink/io_service.h>
-#include <asiolink/unix_domain_socket.h>
-#include <asiolink/unix_domain_socket_acceptor.h>
-#include <asiolink/unix_domain_socket_endpoint.h>
 #include <config/command_mgr.h>
-#include <cc/data.h>
-#include <cc/command_interpreter.h>
-#include <cc/json_feed.h>
-#include <dhcp/iface_mgr.h>
-#include <config/config_log.h>
-#include <config/timeouts.h>
-#include <util/watch_socket.h>
-#include <boost/enable_shared_from_this.hpp>
-#include <array>
-#include <functional>
-#include <unistd.h>
-#include <sys/file.h>
-
-using namespace isc;
-using namespace isc::asiolink;
-using namespace isc::config;
-using namespace isc::data;
-namespace ph = std::placeholders;
-
-namespace {
-
-/// @brief Maximum size of the data chunk sent/received over the socket.
-const size_t BUF_SIZE = 32768;
-
-class ConnectionPool;
-
-/// @brief Represents a single connection over control socket.
-///
-/// An instance of this object is created when the @c CommandMgr acceptor
-/// receives new connection from a controlling client.
-class Connection : public boost::enable_shared_from_this<Connection> {
-public:
-
-    /// @brief Constructor.
-    ///
-    /// This constructor registers a socket of this connection in the Interface
-    /// Manager to cause the blocking call to @c select() to return as soon as
-    /// a transmission over the control socket is received.
-    ///
-    /// It installs two external sockets on the @IfaceMgr to break synchronous
-    /// calls to @select(). The @c WatchSocket is used for send operations
-    /// over the connection. The native socket is used for signaling reads
-    /// over the connection.
-    ///
-    /// @param io_service IOService object used to handle the asio operations
-    /// @param socket Pointer to the object representing a socket which is used
-    /// for data transmission.
-    /// @param connection_pool Reference to the connection pool to which this
-    /// connection belongs.
-    /// @param timeout Connection timeout (in seconds).
-    Connection(const IOServicePtr& io_service,
-               const boost::shared_ptr<UnixDomainSocket>& socket,
-               ConnectionPool& connection_pool,
-               const long timeout)
-        : socket_(socket), timeout_timer_(io_service), timeout_(timeout),
-          buf_(), response_(), connection_pool_(connection_pool), feed_(),
-          response_in_progress_(false), watch_socket_(new util::WatchSocket()) {
-
-        LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_OPENED)
-            .arg(socket_->getNative());
-
-        // Callback value of 0 is used to indicate that callback function is
-        // not installed.
-        isc::dhcp::IfaceMgr::instance().addExternalSocket(watch_socket_->getSelectFd(), 0);
-        isc::dhcp::IfaceMgr::instance().addExternalSocket(socket_->getNative(), 0);
-
-        // Initialize state model for receiving and preparsing commands.
-        feed_.initModel();
-
-        // Start timer for detecting timeouts.
-        scheduleTimer();
-    }
-
-    /// @brief Destructor.
-    ///
-    /// Cancels timeout timer if one is scheduled.
-    ~Connection() {
-        timeout_timer_.cancel();
-    }
-
-    /// @brief This method schedules timer or reschedules existing timer.
-    void scheduleTimer() {
-        timeout_timer_.setup(std::bind(&Connection::timeoutHandler, this),
-                             timeout_, IntervalTimer::ONE_SHOT);
-    }
-
-    /// @brief Close current connection.
-    ///
-    /// Connection is not closed if the invocation of this method is a result of
-    /// server reconfiguration. The connection will be closed once a response is
-    /// sent to the client. Closing a socket during processing a request would
-    /// cause the server to not send a response to the client.
-    void stop() {
-        if (!response_in_progress_) {
-            LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_CLOSED)
-                .arg(socket_->getNative());
-
-            isc::dhcp::IfaceMgr::instance().deleteExternalSocket(watch_socket_->getSelectFd());
-            isc::dhcp::IfaceMgr::instance().deleteExternalSocket(socket_->getNative());
-
-            // Close watch socket and log errors if occur.
-            std::string watch_error;
-            if (!watch_socket_->closeSocket(watch_error)) {
-                LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_CLOSE_ERROR)
-                    .arg(watch_error);
-            }
-
-            socket_->close();
-            timeout_timer_.cancel();
-        }
-    }
-
-    /// @brief Gracefully terminates current connection.
-    ///
-    /// This method should be called prior to closing the socket to initiate
-    /// graceful shutdown.
-    void terminate();
-
-    /// @brief Start asynchronous read over the unix domain socket.
-    ///
-    /// This method doesn't block. Once the transmission is received over the
-    /// socket, the @c Connection::receiveHandler callback is invoked to
-    /// process received data.
-    void doReceive() {
-        socket_->asyncReceive(&buf_[0], sizeof(buf_),
-                              std::bind(&Connection::receiveHandler,
-                                        shared_from_this(), ph::_1, ph::_2));
-    }
-
-    /// @brief Starts asynchronous send over the unix domain socket.
-    ///
-    /// This method doesn't block. Once the send operation (that covers the whole
-    /// data if it's small or first BUF_SIZE bytes if its large) is completed, the
-    /// @c Connection::sendHandler callback is invoked. That handler will either
-    /// close the connection gracefully if all data has been sent, or will
-    /// call @ref doSend() again to send the next chunk of data.
-    void doSend() {
-        size_t chunk_size = (response_.size() < BUF_SIZE) ? response_.size() : BUF_SIZE;
-        socket_->asyncSend(&response_[0], chunk_size,
-           std::bind(&Connection::sendHandler, shared_from_this(), ph::_1, ph::_2));
-
-        // Asynchronous send has been scheduled and we need to indicate this
-        // to break the synchronous select(). The handler should clear this
-        // status when invoked.
-        try {
-            watch_socket_->markReady();
-
-        } catch (const std::exception& ex) {
-            LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_MARK_READY_ERROR)
-                .arg(ex.what());
-        }
-    }
-
-    /// @brief Handler invoked when the data is received over the control
-    /// socket.
-    ///
-    /// It collects received data into the @c isc::config::JSONFeed object and
-    /// schedules additional asynchronous read of data if this object signals
-    /// that command is incomplete. When the entire command is received, the
-    /// handler processes this command and asynchronously responds to the
-    /// controlling client.
-    //
-    ///
-    /// @param ec Error code.
-    /// @param bytes_transferred Number of bytes received.
-    void receiveHandler(const boost::system::error_code& ec,
-                        size_t bytes_transferred);
-
-    /// @brief Handler invoked when the data is sent over the control socket.
-    ///
-    /// If there are still data to be sent, another asynchronous send is
-    /// scheduled. When the entire command is sent, the connection is shutdown
-    /// and closed.
-    ///
-    /// @param ec Error code.
-    /// @param bytes_transferred Number of bytes sent.
-    void sendHandler(const boost::system::error_code& ec,
-                     size_t bytes_transferred);
-
-    /// @brief Handler invoked when timeout has occurred.
-    ///
-    /// Asynchronously sends a response to the client indicating that the
-    /// timeout has occurred.
-    void timeoutHandler();
-
-private:
-
-    /// @brief Pointer to the socket used for transmission.
-    boost::shared_ptr<UnixDomainSocket> socket_;
-
-    /// @brief Interval timer used to detect connection timeouts.
-    IntervalTimer timeout_timer_;
-
-    /// @brief Connection timeout (in milliseconds)
-    long timeout_;
-
-    /// @brief Buffer used for received data.
-    std::array<char, BUF_SIZE> buf_;
-
-    /// @brief Response created by the server.
-    std::string response_;
-
-    /// @brief Reference to the pool of connections.
-    ConnectionPool& connection_pool_;
-
-    /// @brief State model used to receive data over the connection and detect
-    /// when the command ends.
-    JSONFeed feed_;
-
-    /// @brief Boolean flag indicating if the request to stop connection is a
-    /// result of server reconfiguration.
-    bool response_in_progress_;
-
-    /// @brief Pointer to watch socket instance used to signal that the socket
-    /// is ready for read or write.
-    util::WatchSocketPtr watch_socket_;
-};
-
-/// @brief Pointer to the @c Connection.
-typedef boost::shared_ptr<Connection> ConnectionPtr;
-
-/// @brief Holds all open connections.
-class ConnectionPool {
-public:
-
-    /// @brief Starts new connection.
-    ///
-    /// @param connection Pointer to the new connection object.
-    void start(const ConnectionPtr& connection) {
-        connection->doReceive();
-        connections_.insert(connection);
-    }
-
-    /// @brief Stops running connection.
-    ///
-    /// @param connection Pointer to the new connection object.
-    void stop(const ConnectionPtr& connection) {
-        try {
-            connection->stop();
-            connections_.erase(connection);
-        } catch (const std::exception& ex) {
-            LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_CLOSE_FAIL)
-                .arg(ex.what());
-        }
-    }
-
-    /// @brief Stops all connections which are allowed to stop.
-    void stopAll() {
-        for (auto const& conn : connections_) {
-            conn->stop();
-        }
-        connections_.clear();
-    }
-
-private:
-
-    /// @brief Pool of connections.
-    std::set<ConnectionPtr> connections_;
-
-};
-
-void
-Connection::terminate() {
-    try {
-        socket_->shutdown();
-
-    } catch (const std::exception& ex) {
-        LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_SHUTDOWN_FAIL)
-            .arg(ex.what());
-    }
-}
-
-void
-Connection::receiveHandler(const boost::system::error_code& ec,
-                           size_t bytes_transferred) {
-    if (ec) {
-        if (ec.value() == boost::asio::error::eof) {
-            std::stringstream os;
-            if (feed_.getProcessedText().empty()) {
-               os << "no input data to discard";
-            } else {
-               os << "discarding partial command of "
-                  << feed_.getProcessedText().size() << " bytes";
-            }
-
-            // Foreign host has closed the connection. We should remove it from the
-            // connection pool.
-            LOG_INFO(command_logger, COMMAND_SOCKET_CLOSED_BY_FOREIGN_HOST)
-                .arg(socket_->getNative()).arg(os.str());
-        } else if (ec.value() != boost::asio::error::operation_aborted) {
-            LOG_ERROR(command_logger, COMMAND_SOCKET_READ_FAIL)
-                .arg(ec.value()).arg(socket_->getNative());
-        }
-
-        connection_pool_.stop(shared_from_this());
-        return;
-
-    } else if (bytes_transferred == 0) {
-        // Nothing received. Close the connection.
-        connection_pool_.stop(shared_from_this());
-        return;
-    }
-
-    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_READ)
-        .arg(bytes_transferred).arg(socket_->getNative());
-
-    // Reschedule the timer because the transaction is ongoing.
-    scheduleTimer();
-
-    ConstElementPtr cmd;
-    ConstElementPtr rsp;
-
-    try {
-        // Received some data over the socket. Append them to the JSON feed
-        // to see if we have reached the end of command.
-        feed_.postBuffer(&buf_[0], bytes_transferred);
-        feed_.poll();
-        // If we haven't yet received the full command, continue receiving.
-        if (feed_.needData()) {
-            doReceive();
-            return;
-        }
-
-        // Received entire command. Parse the command into JSON.
-        if (feed_.feedOk()) {
-            cmd = feed_.toElement();
-            response_in_progress_ = true;
-
-            // Cancel the timer to make sure that long lasting command
-            // processing doesn't cause the timeout.
-            timeout_timer_.cancel();
-
-            // If successful, then process it as a command.
-            rsp = CommandMgr::instance().processCommand(cmd);
-
-            response_in_progress_ = false;
-
-        } else {
-            // Failed to parse command as JSON or process the received command.
-            // This exception will be caught below and the error response will
-            // be sent.
-            isc_throw(BadValue, feed_.getErrorMessage());
-        }
-
-    } catch (const Exception& ex) {
-        LOG_WARN(command_logger, COMMAND_PROCESS_ERROR1).arg(ex.what());
-        rsp = createAnswer(CONTROL_RESULT_ERROR, std::string(ex.what()));
-    }
-
-    // No response generated. Connection will be closed.
-    if (!rsp) {
-        LOG_WARN(command_logger, COMMAND_RESPONSE_ERROR)
-            .arg(cmd ? cmd->str() : "unknown");
-        rsp = createAnswer(CONTROL_RESULT_ERROR,
-                           "internal server error: no response generated");
-
-    } else {
-
-        // Reschedule the timer as it may be either canceled or need to be
-        // updated to not timeout before we manage to the send the reply.
-        scheduleTimer();
-
-        // Let's convert JSON response to text. Note that at this stage
-        // the rsp pointer is always set.
-        response_ = rsp->str();
-
-        doSend();
-        return;
-    }
-
-    // Close the connection if we have sent the entire response.
-    connection_pool_.stop(shared_from_this());
-}
-
-void
-Connection::sendHandler(const boost::system::error_code& ec,
-                        size_t bytes_transferred) {
-    // Clear the watch socket so as the future send operation can mark it
-    // again to interrupt the synchronous select() call.
-    try {
-        watch_socket_->clearReady();
-
-    } catch (const std::exception& ex) {
-        LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_CLEAR_ERROR)
-            .arg(ex.what());
-    }
-
-    if (ec) {
-        // If an error occurred, log this error and stop the connection.
-        if (ec.value() != boost::asio::error::operation_aborted) {
-            LOG_ERROR(command_logger, COMMAND_SOCKET_WRITE_FAIL)
-                .arg(socket_->getNative()).arg(ec.message());
-        }
-
-    } else {
-
-        // Reschedule the timer because the transaction is ongoing.
-        scheduleTimer();
-
-        // No error. We are in a process of sending a response. Need to
-        // remove the chunk that we have managed to sent with the previous
-        // attempt.
-        response_.erase(0, bytes_transferred);
-
-        LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_WRITE)
-            .arg(bytes_transferred).arg(response_.size())
-            .arg(socket_->getNative());
-
-        // Check if there is any data left to be sent and sent it.
-        if (!response_.empty()) {
-            doSend();
-            return;
-        }
-
-        // Gracefully shutdown the connection and close the socket if
-        // we have sent the whole response.
-        terminate();
-    }
-
-    // All data sent or an error has occurred. Close the connection.
-    connection_pool_.stop(shared_from_this());
-}
-
-void
-Connection::timeoutHandler() {
-    LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_TIMEOUT)
-        .arg(socket_->getNative());
-
-    try {
-        socket_->cancel();
-
-    } catch (const std::exception& ex) {
-        LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_CANCEL_FAIL)
-            .arg(socket_->getNative())
-            .arg(ex.what());
-    }
-
-    std::stringstream os;
-    os << "Connection over control channel timed out";
-    if (!feed_.getProcessedText().empty()) {
-        os << ", discarded partial command of "
-           << feed_.getProcessedText().size() << " bytes";
-    }
-
-    ConstElementPtr rsp = createAnswer(CONTROL_RESULT_ERROR, os.str());
-    response_ = rsp->str();
-    doSend();
-}
-
-}
 
 namespace isc {
 namespace config {
 
-/// @brief Implementation of the @c CommandMgr.
-class CommandMgrImpl {
-public:
-
-    /// @brief Constructor.
-    CommandMgrImpl()
-        : io_service_(), acceptor_(), socket_(), socket_name_(),
-          connection_pool_(), timeout_(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND) {
-    }
-
-    /// @brief Opens acceptor service allowing the control clients to connect.
-    ///
-    /// @param socket_info Configuration information for the control socket.
-    /// @throw BadSocketInfo When socket configuration is invalid.
-    /// @throw SocketError When socket operation fails.
-    void openCommandSocket(const isc::data::ConstElementPtr& socket_info);
-
-    /// @brief Asynchronously accepts next connection.
-    void doAccept();
-
-    /// @brief Returns the lock file name
-    std::string getLockName() {
-        return (std::string(socket_name_ + ".lock"));
-    }
-
-    /// @brief Pointer to the IO service used by the server process for running
-    /// asynchronous tasks.
-    IOServicePtr io_service_;
-
-    /// @brief Pointer to the acceptor service.
-    boost::shared_ptr<UnixDomainSocketAcceptor> acceptor_;
-
-    /// @brief Pointer to the socket into which the new connection is accepted.
-    boost::shared_ptr<UnixDomainSocket> socket_;
-
-    /// @brief Path to the unix domain socket descriptor.
-    ///
-    /// This is used to remove the socket file once the connection terminates.
-    std::string socket_name_;
-
-    /// @brief Pool of connections.
-    ConnectionPool connection_pool_;
-
-    /// @brief Connection timeout
-    long timeout_;
-};
-
-void
-CommandMgrImpl::openCommandSocket(const isc::data::ConstElementPtr& socket_info) {
-    socket_name_.clear();
-
-    if(!socket_info) {
-        isc_throw(BadSocketInfo, "Missing socket_info parameters, can't create socket.");
-    }
-
-    ConstElementPtr type = socket_info->get("socket-type");
-    if (!type) {
-        isc_throw(BadSocketInfo, "Mandatory 'socket-type' parameter missing");
-    }
-
-    // Only supporting unix sockets right now.
-    if (type->stringValue() != "unix") {
-        isc_throw(BadSocketInfo, "Invalid 'socket-type' parameter value "
-                  << type->stringValue());
-    }
-
-    // UNIX socket is requested. It takes one parameter: socket-name that
-    // specifies UNIX path of the socket.
-    ConstElementPtr name = socket_info->get("socket-name");
-    if (!name) {
-        isc_throw(BadSocketInfo, "Mandatory 'socket-name' parameter missing");
-    }
-
-    if (name->getType() != Element::string) {
-        isc_throw(BadSocketInfo, "'socket-name' parameter expected to be a string");
-    }
-
-    socket_name_ = name->stringValue();
-
-    // First let's open lock file.
-    std::string lock_name = getLockName();
-    int lock_fd = open(lock_name.c_str(), O_RDONLY | O_CREAT, 0600);
-    if (lock_fd == -1) {
-        std::string errmsg = strerror(errno);
-        isc_throw(SocketError, "cannot create socket lockfile, "
-                  << lock_name  << ", : " << errmsg);
-    }
-
-    // Try to acquire lock. If we can't somebody else is actively
-    // using it.
-    int ret = flock(lock_fd, LOCK_EX | LOCK_NB);
-    if (ret != 0) {
-        std::string errmsg = strerror(errno);
-        isc_throw(SocketError, "cannot lock socket lockfile, "
-                  << lock_name  << ", : " << errmsg);
-    }
-
-    // We have the lock, so let's remove the pre-existing socket
-    // file if it exists.
-    static_cast<void>(::remove(socket_name_.c_str()));
-
-    LOG_INFO(command_logger, COMMAND_ACCEPTOR_START)
-        .arg(socket_name_);
-
-    try {
-        // Start asynchronous acceptor service.
-        acceptor_.reset(new UnixDomainSocketAcceptor(io_service_));
-        UnixDomainSocketEndpoint endpoint(socket_name_);
-        acceptor_->open(endpoint);
-        acceptor_->bind(endpoint);
-        acceptor_->listen();
-        // Install this socket in Interface Manager.
-        isc::dhcp::IfaceMgr::instance().addExternalSocket(acceptor_->getNative(), 0);
-
-        doAccept();
-
-    } catch (const std::exception& ex) {
-        isc_throw(SocketError, ex.what());
-    }
-}
-
-void
-CommandMgrImpl::doAccept() {
-    // Create a socket into which the acceptor will accept new connection.
-    socket_.reset(new UnixDomainSocket(io_service_));
-    acceptor_->asyncAccept(*socket_, [this](const boost::system::error_code& ec) {
-        if (!ec) {
-            // New connection is arriving. Start asynchronous transmission.
-            ConnectionPtr connection(new Connection(io_service_, socket_,
-                                                    connection_pool_,
-                                                    timeout_));
-            connection_pool_.start(connection);
-
-        } else if (ec.value() != boost::asio::error::operation_aborted) {
-            LOG_ERROR(command_logger, COMMAND_SOCKET_ACCEPT_FAIL)
-                .arg(acceptor_->getNative()).arg(ec.message());
-        }
-
-        // Unless we're stopping the service, start accepting connections again.
-        if (ec.value() != boost::asio::error::operation_aborted) {
-            doAccept();
-        }
-    });
-}
-
-CommandMgr::CommandMgr()
-    : HookedCommandMgr(), impl_(new CommandMgrImpl()) {
-}
-
-void
-CommandMgr::openCommandSocket(const isc::data::ConstElementPtr& socket_info) {
-    impl_->openCommandSocket(socket_info);
-}
-
-void CommandMgr::closeCommandSocket() {
-    // Close acceptor if the acceptor is open.
-    if (impl_->acceptor_ && impl_->acceptor_->isOpen()) {
-        isc::dhcp::IfaceMgr::instance().deleteExternalSocket(impl_->acceptor_->getNative());
-        impl_->acceptor_->close();
-        static_cast<void>(::remove(impl_->socket_name_.c_str()));
-        static_cast<void>(::remove(impl_->getLockName().c_str()));
-    }
-
-    // Stop all connections which can be closed. The only connection that won't
-    // be closed is the one over which we have received a request to reconfigure
-    // the server. This connection will be held until the CommandMgr responds to
-    // such request.
-    impl_->connection_pool_.stopAll();
-}
-
-int
-CommandMgr::getControlSocketFD() {
-    return (impl_->acceptor_ ? impl_->acceptor_->getNative() : -1);
+CommandMgr::CommandMgr() : HookedCommandMgr() {
 }
 
 CommandMgr&
@@ -648,15 +20,5 @@ CommandMgr::instance() {
     return (cmd_mgr);
 }
 
-void
-CommandMgr::setIOService(const IOServicePtr& io_service) {
-    impl_->io_service_ = io_service;
-}
-
-void
-CommandMgr::setConnectionTimeout(const long timeout) {
-    impl_->timeout_ = timeout;
-}
-
 } // end of isc::config
 } // end of isc
index 91f2c8f39a8bd74a06f5c00c72aae25b643b8289..10f2f0a12d1977a9843c06d70b00ab7e199f56fc 100644 (file)
@@ -7,33 +7,12 @@
 #ifndef COMMAND_MGR_H
 #define COMMAND_MGR_H
 
-#include <asiolink/io_service.h>
-#include <cc/data.h>
 #include <config/hooked_command_mgr.h>
-#include <exceptions/exceptions.h>
 #include <boost/noncopyable.hpp>
-#include <boost/shared_ptr.hpp>
 
 namespace isc {
 namespace config {
 
-/// @brief An exception indicating that specified socket parameters are invalid
-class BadSocketInfo : public Exception {
-public:
-    BadSocketInfo(const char* file, size_t line, const char* what) :
-        isc::Exception(file, line, what) { };
-};
-
-/// @brief An exception indicating a problem with socket operation
-class SocketError : public Exception {
-public:
-    SocketError(const char* file, size_t line, const char* what) :
-        isc::Exception(file, line, what) { };
-};
-
-
-class CommandMgrImpl;
-
 /// @brief Commands Manager implementation for the Kea servers.
 ///
 /// This class extends @ref BaseCommandMgr with the ability to receive and
@@ -44,48 +23,13 @@ public:
     /// @brief CommandMgr is a singleton class. This method returns reference
     /// to its sole instance.
     ///
-    /// @return the only existing instance of the manager
+    /// @return the only existing instance of the manager.
     static CommandMgr& instance();
 
-    /// @brief Sets IO service to be used by the command manager.
-    ///
-    /// The server should use this method to provide the Command Manager with the
-    /// common IO service used by the server.
-    /// @param io_service Pointer to the IO service.
-    void setIOService(const asiolink::IOServicePtr& io_service);
-
-    /// @brief Override default connection timeout.
-    ///
-    /// @param timeout New connection timeout in milliseconds.
-    void setConnectionTimeout(const long timeout);
-
-    /// @brief Opens control socket with parameters specified in socket_info
-    ///
-    /// Currently supported types are:
-    /// - unix (required parameters: socket-type: unix, socket-name:/unix/path)
-    ///
-    /// @throw BadSocketInfo When socket configuration is invalid.
-    /// @throw SocketError When socket operation fails.
-    ///
-    /// @param socket_info Configuration information for the control socket.
-    void
-    openCommandSocket(const isc::data::ConstElementPtr& socket_info);
-
-    /// @brief Shuts down any open control sockets
-    void closeCommandSocket();
-
-    /// @brief Returns control socket descriptor
-    ///
-    /// This method should be used only in tests.
-    int getControlSocketFD();
-
 private:
 
-    /// @brief Private constructor
+    /// @brief Private constructor.
     CommandMgr();
-
-    /// @brief Pointer to the implementation of the @ref CommandMgr.
-    boost::shared_ptr<CommandMgrImpl> impl_;
 };
 
 } // end of isc::config namespace
index fafe50eb11356d1ce07a1343c75545fc3ad59963..2f396ae83e333e67832d5655eb5c44d3e92aedfe 100644 (file)
@@ -215,8 +215,7 @@ HttpCommandMgr::instance() {
     return (http_cmd_mgr);
 }
 
-HttpCommandMgr::HttpCommandMgr()
-    : HookedCommandMgr(), impl_(new HttpCommandMgrImpl()) {
+HttpCommandMgr::HttpCommandMgr() : impl_(new HttpCommandMgrImpl()) {
 }
 
 void
index 9cd51707a63f568c911b3cd6d8bdc9441cb10adc..12b2bf3b9f920babc7a84b23ed137c5d67109aa2 100644 (file)
@@ -9,7 +9,6 @@
 
 #include <asiolink/io_service.h>
 #include <config/http_command_config.h>
-#include <config/hooked_command_mgr.h>
 #include <http/listener.h>
 #include <boost/noncopyable.hpp>
 
@@ -21,8 +20,8 @@ class HttpCommandMgrImpl;
 
 /// @brief HTTP Commands Manager implementation for the Kea servers.
 ///
-/// Similar to @c CommandMgr but using HTTP/HTTPS instead of UNIX sockets.
-class HttpCommandMgr : public HookedCommandMgr, public boost::noncopyable {
+/// Similar to @c UnixCommandMgr but using HTTP/HTTPS instead of UNIX sockets.
+class HttpCommandMgr : public boost::noncopyable {
 public:
 
     /// @brief HttpCommandMgr is a singleton class. This method
@@ -31,7 +30,7 @@ public:
     /// @return The only existing instance of the manager.
     static HttpCommandMgr& instance();
 
-    /// @brief Sets IO service to be used by the command manager.
+    /// @brief Sets IO service to be used by the http command manager.
     ///
     /// The server should use this method to provide the Command
     /// Manager with the common IO service used by the server.
@@ -56,12 +55,12 @@ public:
     /// @param use_external True (default) add external sockets.
     void addExternalSockets(bool use_external = true);
 
-    /// @brief Configure control socket from configuration.
+    /// @brief Configure http control socket from configuration.
     ///
-    /// @param config Configuration of the control socket.
+    /// @param config Configuration of the control http socket.
     void configure(HttpCommandConfigPtr config);
 
-    /// @brief Close control socket.
+    /// @brief Close http control socket.
     ///
     /// @note When remove is false @c garbageCollectListeners must
     /// be called after.
index 41077ce9533f3916e8632659d3170a2401cef5f6..3a95de4d2df209d26aaa8d886965c94e6e21cabe 100644 (file)
@@ -21,6 +21,7 @@ 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 += unix_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
index 492bcfa825f9540d4187e9cf1af995c1dc8693e6..289548521f8c4db3fbe755136e8c42ced13fe7e8 100644 (file)
@@ -8,8 +8,6 @@
 
 #include <gtest/gtest.h>
 
-#include <testutils/sandbox.h>
-#include <asiolink/io_service.h>
 #include <config/base_command_mgr.h>
 #include <config/command_mgr.h>
 #include <config/hooked_command_mgr.h>
@@ -20,7 +18,6 @@
 #include <string>
 #include <vector>
 
-using namespace isc::asiolink;
 using namespace isc::config;
 using namespace isc::data;
 using namespace isc::hooks;
@@ -29,14 +26,8 @@ using namespace std;
 // Test class for Command Manager
 class CommandMgrTest : public ::testing::Test {
 public:
-    isc::test::Sandbox sandbox;
-
     /// Default constructor
-    CommandMgrTest()
-        : io_service_(new IOService()) {
-
-        CommandMgr::instance().setIOService(io_service_);
-
+    CommandMgrTest() {
         handler_name_ = "";
         handler_params_ = ElementPtr();
         handler_called_ = false;
@@ -45,7 +36,6 @@ public:
         processed_log_ = "";
 
         CommandMgr::instance().deregisterAll();
-        CommandMgr::instance().closeCommandSocket();
 
         resetCalloutIndicators();
     }
@@ -53,23 +43,9 @@ public:
     /// Default destructor
     virtual ~CommandMgrTest() {
         CommandMgr::instance().deregisterAll();
-        CommandMgr::instance().closeCommandSocket();
         resetCalloutIndicators();
     }
 
-    /// @brief Returns socket path (using either hardcoded path or env variable)
-    /// @return path to the unix socket
-    std::string getSocketPath() {
-        std::string socket_path;
-        const char* env = getenv("KEA_SOCKET_TEST_DIR");
-        if (env) {
-            socket_path = std::string(env) + "/test-socket";
-        } else {
-            socket_path = sandbox.join("test-socket");
-        }
-        return (socket_path);
-    }
-
     /// @brief Resets indicators related to callout invocation.
     ///
     /// It also removes any registered callouts.
@@ -150,9 +126,6 @@ public:
         return (0);
     }
 
-    /// @brief IO service used by these tests.
-    IOServicePtr io_service_;
-
     /// @brief Name of the command (used in my_handler)
     static std::string handler_name_;
 
@@ -427,46 +400,6 @@ TEST_F(CommandMgrTest, delegateListCommands) {
     EXPECT_EQ("my-command-bis", command_names_list[2]);
 }
 
-// This test verifies that a Unix socket can be opened properly and that input
-// parameters (socket-type and socket-name) are verified.
-TEST_F(CommandMgrTest, unixCreate) {
-    // Null pointer is obviously a bad idea.
-    EXPECT_THROW(CommandMgr::instance().openCommandSocket(ConstElementPtr()),
-                 isc::config::BadSocketInfo);
-
-    // So is passing no parameters.
-    ElementPtr socket_info = Element::createMap();
-    EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info),
-                 isc::config::BadSocketInfo);
-
-    // We don't support ipx sockets
-    socket_info->set("socket-type", Element::create("ipx"));
-    EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info),
-                 isc::config::BadSocketInfo);
-
-    socket_info->set("socket-type", Element::create("unix"));
-    EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info),
-                 isc::config::BadSocketInfo);
-
-    socket_info->set("socket-name", Element::create(getSocketPath()));
-    EXPECT_NO_THROW(CommandMgr::instance().openCommandSocket(socket_info));
-    EXPECT_GE(CommandMgr::instance().getControlSocketFD(), 0);
-
-    // It should be possible to close the socket.
-    EXPECT_NO_THROW(CommandMgr::instance().closeCommandSocket());
-}
-
-// This test checks that when unix path is too long, the socket cannot be opened.
-TEST_F(CommandMgrTest, unixCreateTooLong) {
-    ElementPtr socket_info = Element::fromJSON("{ \"socket-type\": \"unix\","
-        "\"socket-name\": \"/tmp/toolongtoolongtoolongtoolongtoolongtoolong"
-        "toolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolong"
-        "\" }");
-
-    EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info),
-                 SocketError);
-}
-
 // This test verifies that a registered callout for the command_processed
 // hookpoint is invoked and passed the correct information.
 TEST_F(CommandMgrTest, commandProcessedHook) {
@@ -542,28 +475,3 @@ TEST_F(CommandMgrTest, commandProcessedHookReplaceResponse) {
              "{ \"result\": 2, \"text\": \"'change-response' command not supported.\" }",
               processed_log_);
 }
-
-// Verifies that a socket cannot be concurrently opened more than once.
-TEST_F(CommandMgrTest, exclusiveOpen) {
-    // Pass in valid parameters.
-    ElementPtr socket_info = Element::createMap();
-    socket_info->set("socket-type", Element::create("unix"));
-    socket_info->set("socket-name", Element::create(getSocketPath()));
-
-    EXPECT_NO_THROW(CommandMgr::instance().openCommandSocket(socket_info));
-    EXPECT_GE(CommandMgr::instance().getControlSocketFD(), 0);
-
-    // Should not be able to open it twice.
-    EXPECT_THROW(CommandMgr::instance().openCommandSocket(socket_info),
-                 isc::config::SocketError);
-
-    // Now let's close it.
-    EXPECT_NO_THROW(CommandMgr::instance().closeCommandSocket());
-
-    // Should be able to re-open it now.
-    EXPECT_NO_THROW(CommandMgr::instance().openCommandSocket(socket_info));
-    EXPECT_GE(CommandMgr::instance().getControlSocketFD(), 0);
-
-    // Now let's close it.
-    EXPECT_NO_THROW(CommandMgr::instance().closeCommandSocket());
-}
diff --git a/src/lib/config/tests/unix_command_mgr_unittests.cc b/src/lib/config/tests/unix_command_mgr_unittests.cc
new file mode 100644 (file)
index 0000000..9b59fb6
--- /dev/null
@@ -0,0 +1,117 @@
+// Copyright (C) 2015-2024 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 <gtest/gtest.h>
+
+#include <testutils/sandbox.h>
+#include <asiolink/io_service.h>
+#include <config/unix_command_mgr.h>
+#include <string>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace std;
+
+// Test class for Unix Command Manager
+class UnixCommandMgrTest : public ::testing::Test {
+public:
+    isc::test::Sandbox sandbox;
+
+    /// Default constructor
+    UnixCommandMgrTest() : io_service_(new IOService()) {
+        UnixCommandMgr::instance().setIOService(io_service_);
+        UnixCommandMgr::instance().closeCommandSocket();
+    }
+
+    /// Default destructor
+    virtual ~UnixCommandMgrTest() {
+        UnixCommandMgr::instance().closeCommandSocket();
+    }
+
+    /// @brief Returns socket path (using either hardcoded path or env variable)
+    /// @return path to the unix socket
+    std::string getSocketPath() {
+        std::string socket_path;
+        const char* env = getenv("KEA_SOCKET_TEST_DIR");
+        if (env) {
+            socket_path = std::string(env) + "/test-socket";
+        } else {
+            socket_path = sandbox.join("test-socket");
+        }
+        return (socket_path);
+    }
+
+    /// @brief IO service used by these tests.
+    IOServicePtr io_service_;
+};
+
+// This test verifies that a Unix socket can be opened properly and that input
+// parameters (socket-type and socket-name) are verified.
+TEST_F(UnixCommandMgrTest, unixCreate) {
+    // Null pointer is obviously a bad idea.
+    EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(ConstElementPtr()),
+                 isc::config::BadSocketInfo);
+
+    // So is passing no parameters.
+    ElementPtr socket_info = Element::createMap();
+    EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info),
+                 isc::config::BadSocketInfo);
+
+    // We don't support ipx sockets
+    socket_info->set("socket-type", Element::create("ipx"));
+    EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info),
+                 isc::config::BadSocketInfo);
+
+    socket_info->set("socket-type", Element::create("unix"));
+    EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info),
+                 isc::config::BadSocketInfo);
+
+    socket_info->set("socket-name", Element::create(getSocketPath()));
+    EXPECT_NO_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info));
+    EXPECT_GE(UnixCommandMgr::instance().getControlSocketFD(), 0);
+
+    // It should be possible to close the socket.
+    EXPECT_NO_THROW(UnixCommandMgr::instance().closeCommandSocket());
+}
+
+// This test checks that when unix path is too long, the socket cannot be opened.
+TEST_F(UnixCommandMgrTest, unixCreateTooLong) {
+    ElementPtr socket_info = Element::fromJSON("{ \"socket-type\": \"unix\","
+        "\"socket-name\": \"/tmp/toolongtoolongtoolongtoolongtoolongtoolong"
+        "toolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolong"
+        "\" }");
+
+    EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info),
+                 SocketError);
+}
+
+// Verifies that a socket cannot be concurrently opened more than once.
+TEST_F(UnixCommandMgrTest, exclusiveOpen) {
+    // Pass in valid parameters.
+    ElementPtr socket_info = Element::createMap();
+    socket_info->set("socket-type", Element::create("unix"));
+    socket_info->set("socket-name", Element::create(getSocketPath()));
+
+    EXPECT_NO_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info));
+    EXPECT_GE(UnixCommandMgr::instance().getControlSocketFD(), 0);
+
+    // Should not be able to open it twice.
+    EXPECT_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info),
+                 isc::config::SocketError);
+
+    // Now let's close it.
+    EXPECT_NO_THROW(UnixCommandMgr::instance().closeCommandSocket());
+
+    // Should be able to re-open it now.
+    EXPECT_NO_THROW(UnixCommandMgr::instance().openCommandSocket(socket_info));
+    EXPECT_GE(UnixCommandMgr::instance().getControlSocketFD(), 0);
+
+    // Now let's close it.
+    EXPECT_NO_THROW(UnixCommandMgr::instance().closeCommandSocket());
+}
diff --git a/src/lib/config/unix_command_mgr.cc b/src/lib/config/unix_command_mgr.cc
new file mode 100644 (file)
index 0000000..94ed6f0
--- /dev/null
@@ -0,0 +1,663 @@
+// Copyright (C) 2015-2024 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/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <asiolink/unix_domain_socket.h>
+#include <asiolink/unix_domain_socket_acceptor.h>
+#include <asiolink/unix_domain_socket_endpoint.h>
+#include <config/command_mgr.h>
+#include <config/unix_command_mgr.h>
+#include <cc/data.h>
+#include <cc/command_interpreter.h>
+#include <cc/json_feed.h>
+#include <dhcp/iface_mgr.h>
+#include <config/config_log.h>
+#include <config/timeouts.h>
+#include <util/watch_socket.h>
+#include <boost/enable_shared_from_this.hpp>
+#include <array>
+#include <functional>
+#include <unistd.h>
+#include <sys/file.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+namespace ph = std::placeholders;
+
+namespace {
+
+/// @brief Maximum size of the data chunk sent/received over the socket.
+const size_t BUF_SIZE = 32768;
+
+class ConnectionPool;
+
+/// @brief Represents a single connection over control socket.
+///
+/// An instance of this object is created when the @c CommandMgr acceptor
+/// receives new connection from a controlling client.
+class Connection : public boost::enable_shared_from_this<Connection> {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// This constructor registers a socket of this connection in the Interface
+    /// Manager to cause the blocking call to @c select() to return as soon as
+    /// a transmission over the control socket is received.
+    ///
+    /// It installs two external sockets on the @IfaceMgr to break synchronous
+    /// calls to @select(). The @c WatchSocket is used for send operations
+    /// over the connection. The native socket is used for signaling reads
+    /// over the connection.
+    ///
+    /// @param io_service IOService object used to handle the asio operations
+    /// @param socket Pointer to the object representing a socket which is used
+    /// for data transmission.
+    /// @param connection_pool Reference to the connection pool to which this
+    /// connection belongs.
+    /// @param timeout Connection timeout (in seconds).
+    Connection(const IOServicePtr& io_service,
+               const boost::shared_ptr<UnixDomainSocket>& socket,
+               ConnectionPool& connection_pool,
+               const long timeout)
+        : socket_(socket), timeout_timer_(io_service), timeout_(timeout),
+          buf_(), response_(), connection_pool_(connection_pool), feed_(),
+          response_in_progress_(false), watch_socket_(new util::WatchSocket()) {
+
+        LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_OPENED)
+            .arg(socket_->getNative());
+
+        // Callback value of 0 is used to indicate that callback function is
+        // not installed.
+        isc::dhcp::IfaceMgr::instance().addExternalSocket(watch_socket_->getSelectFd(), 0);
+        isc::dhcp::IfaceMgr::instance().addExternalSocket(socket_->getNative(), 0);
+
+        // Initialize state model for receiving and preparsing commands.
+        feed_.initModel();
+
+        // Start timer for detecting timeouts.
+        scheduleTimer();
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Cancels timeout timer if one is scheduled.
+    ~Connection() {
+        timeout_timer_.cancel();
+    }
+
+    /// @brief This method schedules timer or reschedules existing timer.
+    void scheduleTimer() {
+        timeout_timer_.setup(std::bind(&Connection::timeoutHandler, this),
+                             timeout_, IntervalTimer::ONE_SHOT);
+    }
+
+    /// @brief Close current connection.
+    ///
+    /// Connection is not closed if the invocation of this method is a result of
+    /// server reconfiguration. The connection will be closed once a response is
+    /// sent to the client. Closing a socket during processing a request would
+    /// cause the server to not send a response to the client.
+    void stop() {
+        if (!response_in_progress_) {
+            LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_CONNECTION_CLOSED)
+                .arg(socket_->getNative());
+
+            isc::dhcp::IfaceMgr::instance().deleteExternalSocket(watch_socket_->getSelectFd());
+            isc::dhcp::IfaceMgr::instance().deleteExternalSocket(socket_->getNative());
+
+            // Close watch socket and log errors if occur.
+            std::string watch_error;
+            if (!watch_socket_->closeSocket(watch_error)) {
+                LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_CLOSE_ERROR)
+                    .arg(watch_error);
+            }
+
+            socket_->close();
+            timeout_timer_.cancel();
+        }
+    }
+
+    /// @brief Gracefully terminates current connection.
+    ///
+    /// This method should be called prior to closing the socket to initiate
+    /// graceful shutdown.
+    void terminate();
+
+    /// @brief Start asynchronous read over the unix domain socket.
+    ///
+    /// This method doesn't block. Once the transmission is received over the
+    /// socket, the @c Connection::receiveHandler callback is invoked to
+    /// process received data.
+    void doReceive() {
+        socket_->asyncReceive(&buf_[0], sizeof(buf_),
+                              std::bind(&Connection::receiveHandler,
+                                        shared_from_this(), ph::_1, ph::_2));
+    }
+
+    /// @brief Starts asynchronous send over the unix domain socket.
+    ///
+    /// This method doesn't block. Once the send operation (that covers the whole
+    /// data if it's small or first BUF_SIZE bytes if its large) is completed, the
+    /// @c Connection::sendHandler callback is invoked. That handler will either
+    /// close the connection gracefully if all data has been sent, or will
+    /// call @ref doSend() again to send the next chunk of data.
+    void doSend() {
+        size_t chunk_size = (response_.size() < BUF_SIZE) ? response_.size() : BUF_SIZE;
+        socket_->asyncSend(&response_[0], chunk_size,
+           std::bind(&Connection::sendHandler, shared_from_this(), ph::_1, ph::_2));
+
+        // Asynchronous send has been scheduled and we need to indicate this
+        // to break the synchronous select(). The handler should clear this
+        // status when invoked.
+        try {
+            watch_socket_->markReady();
+
+        } catch (const std::exception& ex) {
+            LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_MARK_READY_ERROR)
+                .arg(ex.what());
+        }
+    }
+
+    /// @brief Handler invoked when the data is received over the control
+    /// socket.
+    ///
+    /// It collects received data into the @c isc::config::JSONFeed object and
+    /// schedules additional asynchronous read of data if this object signals
+    /// that command is incomplete. When the entire command is received, the
+    /// handler processes this command and asynchronously responds to the
+    /// controlling client.
+    //
+    ///
+    /// @param ec Error code.
+    /// @param bytes_transferred Number of bytes received.
+    void receiveHandler(const boost::system::error_code& ec,
+                        size_t bytes_transferred);
+
+    /// @brief Handler invoked when the data is sent over the control socket.
+    ///
+    /// If there are still data to be sent, another asynchronous send is
+    /// scheduled. When the entire command is sent, the connection is shutdown
+    /// and closed.
+    ///
+    /// @param ec Error code.
+    /// @param bytes_transferred Number of bytes sent.
+    void sendHandler(const boost::system::error_code& ec,
+                     size_t bytes_transferred);
+
+    /// @brief Handler invoked when timeout has occurred.
+    ///
+    /// Asynchronously sends a response to the client indicating that the
+    /// timeout has occurred.
+    void timeoutHandler();
+
+private:
+
+    /// @brief Pointer to the socket used for transmission.
+    boost::shared_ptr<UnixDomainSocket> socket_;
+
+    /// @brief Interval timer used to detect connection timeouts.
+    IntervalTimer timeout_timer_;
+
+    /// @brief Connection timeout (in milliseconds)
+    long timeout_;
+
+    /// @brief Buffer used for received data.
+    std::array<char, BUF_SIZE> buf_;
+
+    /// @brief Response created by the server.
+    std::string response_;
+
+    /// @brief Reference to the pool of connections.
+    ConnectionPool& connection_pool_;
+
+    /// @brief State model used to receive data over the connection and detect
+    /// when the command ends.
+    JSONFeed feed_;
+
+    /// @brief Boolean flag indicating if the request to stop connection is a
+    /// result of server reconfiguration.
+    bool response_in_progress_;
+
+    /// @brief Pointer to watch socket instance used to signal that the socket
+    /// is ready for read or write.
+    util::WatchSocketPtr watch_socket_;
+};
+
+/// @brief Pointer to the @c Connection.
+typedef boost::shared_ptr<Connection> ConnectionPtr;
+
+/// @brief Holds all open connections.
+class ConnectionPool {
+public:
+
+    /// @brief Starts new connection.
+    ///
+    /// @param connection Pointer to the new connection object.
+    void start(const ConnectionPtr& connection) {
+        connection->doReceive();
+        connections_.insert(connection);
+    }
+
+    /// @brief Stops running connection.
+    ///
+    /// @param connection Pointer to the new connection object.
+    void stop(const ConnectionPtr& connection) {
+        try {
+            connection->stop();
+            connections_.erase(connection);
+        } catch (const std::exception& ex) {
+            LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_CLOSE_FAIL)
+                .arg(ex.what());
+        }
+    }
+
+    /// @brief Stops all connections which are allowed to stop.
+    void stopAll() {
+        for (auto const& conn : connections_) {
+            conn->stop();
+        }
+        connections_.clear();
+    }
+
+private:
+
+    /// @brief Pool of connections.
+    std::set<ConnectionPtr> connections_;
+
+};
+
+void
+Connection::terminate() {
+    try {
+        socket_->shutdown();
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_SHUTDOWN_FAIL)
+            .arg(ex.what());
+    }
+}
+
+void
+Connection::receiveHandler(const boost::system::error_code& ec,
+                           size_t bytes_transferred) {
+    if (ec) {
+        if (ec.value() == boost::asio::error::eof) {
+            std::stringstream os;
+            if (feed_.getProcessedText().empty()) {
+               os << "no input data to discard";
+            } else {
+               os << "discarding partial command of "
+                  << feed_.getProcessedText().size() << " bytes";
+            }
+
+            // Foreign host has closed the connection. We should remove it from the
+            // connection pool.
+            LOG_INFO(command_logger, COMMAND_SOCKET_CLOSED_BY_FOREIGN_HOST)
+                .arg(socket_->getNative()).arg(os.str());
+        } else if (ec.value() != boost::asio::error::operation_aborted) {
+            LOG_ERROR(command_logger, COMMAND_SOCKET_READ_FAIL)
+                .arg(ec.value()).arg(socket_->getNative());
+        }
+
+        connection_pool_.stop(shared_from_this());
+        return;
+
+    } else if (bytes_transferred == 0) {
+        // Nothing received. Close the connection.
+        connection_pool_.stop(shared_from_this());
+        return;
+    }
+
+    LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_READ)
+        .arg(bytes_transferred).arg(socket_->getNative());
+
+    // Reschedule the timer because the transaction is ongoing.
+    scheduleTimer();
+
+    ConstElementPtr cmd;
+    ConstElementPtr rsp;
+
+    try {
+        // Received some data over the socket. Append them to the JSON feed
+        // to see if we have reached the end of command.
+        feed_.postBuffer(&buf_[0], bytes_transferred);
+        feed_.poll();
+        // If we haven't yet received the full command, continue receiving.
+        if (feed_.needData()) {
+            doReceive();
+            return;
+        }
+
+        // Received entire command. Parse the command into JSON.
+        if (feed_.feedOk()) {
+            cmd = feed_.toElement();
+            response_in_progress_ = true;
+
+            // Cancel the timer to make sure that long lasting command
+            // processing doesn't cause the timeout.
+            timeout_timer_.cancel();
+
+            // If successful, then process it as a command.
+            rsp = CommandMgr::instance().processCommand(cmd);
+
+            response_in_progress_ = false;
+
+        } else {
+            // Failed to parse command as JSON or process the received command.
+            // This exception will be caught below and the error response will
+            // be sent.
+            isc_throw(BadValue, feed_.getErrorMessage());
+        }
+
+    } catch (const Exception& ex) {
+        LOG_WARN(command_logger, COMMAND_PROCESS_ERROR1).arg(ex.what());
+        rsp = createAnswer(CONTROL_RESULT_ERROR, std::string(ex.what()));
+    }
+
+    // No response generated. Connection will be closed.
+    if (!rsp) {
+        LOG_WARN(command_logger, COMMAND_RESPONSE_ERROR)
+            .arg(cmd ? cmd->str() : "unknown");
+        rsp = createAnswer(CONTROL_RESULT_ERROR,
+                           "internal server error: no response generated");
+
+    } else {
+
+        // Reschedule the timer as it may be either canceled or need to be
+        // updated to not timeout before we manage to the send the reply.
+        scheduleTimer();
+
+        // Let's convert JSON response to text. Note that at this stage
+        // the rsp pointer is always set.
+        response_ = rsp->str();
+
+        doSend();
+        return;
+    }
+
+    // Close the connection if we have sent the entire response.
+    connection_pool_.stop(shared_from_this());
+}
+
+void
+Connection::sendHandler(const boost::system::error_code& ec,
+                        size_t bytes_transferred) {
+    // Clear the watch socket so as the future send operation can mark it
+    // again to interrupt the synchronous select() call.
+    try {
+        watch_socket_->clearReady();
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(command_logger, COMMAND_WATCH_SOCKET_CLEAR_ERROR)
+            .arg(ex.what());
+    }
+
+    if (ec) {
+        // If an error occurred, log this error and stop the connection.
+        if (ec.value() != boost::asio::error::operation_aborted) {
+            LOG_ERROR(command_logger, COMMAND_SOCKET_WRITE_FAIL)
+                .arg(socket_->getNative()).arg(ec.message());
+        }
+
+    } else {
+
+        // Reschedule the timer because the transaction is ongoing.
+        scheduleTimer();
+
+        // No error. We are in a process of sending a response. Need to
+        // remove the chunk that we have managed to sent with the previous
+        // attempt.
+        response_.erase(0, bytes_transferred);
+
+        LOG_DEBUG(command_logger, DBG_COMMAND, COMMAND_SOCKET_WRITE)
+            .arg(bytes_transferred).arg(response_.size())
+            .arg(socket_->getNative());
+
+        // Check if there is any data left to be sent and sent it.
+        if (!response_.empty()) {
+            doSend();
+            return;
+        }
+
+        // Gracefully shutdown the connection and close the socket if
+        // we have sent the whole response.
+        terminate();
+    }
+
+    // All data sent or an error has occurred. Close the connection.
+    connection_pool_.stop(shared_from_this());
+}
+
+void
+Connection::timeoutHandler() {
+    LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_TIMEOUT)
+        .arg(socket_->getNative());
+
+    try {
+        socket_->cancel();
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(command_logger, COMMAND_SOCKET_CONNECTION_CANCEL_FAIL)
+            .arg(socket_->getNative())
+            .arg(ex.what());
+    }
+
+    std::stringstream os;
+    os << "Connection over control channel timed out";
+    if (!feed_.getProcessedText().empty()) {
+        os << ", discarded partial command of "
+           << feed_.getProcessedText().size() << " bytes";
+    }
+
+    ConstElementPtr rsp = createAnswer(CONTROL_RESULT_ERROR, os.str());
+    response_ = rsp->str();
+    doSend();
+}
+
+}
+
+namespace isc {
+namespace config {
+
+/// @brief Implementation of the @c UnixCommandMgr.
+class UnixCommandMgrImpl {
+public:
+
+    /// @brief Constructor.
+    UnixCommandMgrImpl()
+        : io_service_(), acceptor_(), socket_(), socket_name_(),
+          connection_pool_(), timeout_(TIMEOUT_DHCP_SERVER_RECEIVE_COMMAND) {
+    }
+
+    /// @brief Opens acceptor service allowing the control clients to connect.
+    ///
+    /// @param socket_info Configuration information for the control socket.
+    /// @throw BadSocketInfo When socket configuration is invalid.
+    /// @throw SocketError When socket operation fails.
+    void openCommandSocket(const isc::data::ConstElementPtr& socket_info);
+
+    /// @brief Asynchronously accepts next connection.
+    void doAccept();
+
+    /// @brief Returns the lock file name
+    std::string getLockName() {
+        return (std::string(socket_name_ + ".lock"));
+    }
+
+    /// @brief Pointer to the IO service used by the server process for running
+    /// asynchronous tasks.
+    IOServicePtr io_service_;
+
+    /// @brief Pointer to the acceptor service.
+    boost::shared_ptr<UnixDomainSocketAcceptor> acceptor_;
+
+    /// @brief Pointer to the socket into which the new connection is accepted.
+    boost::shared_ptr<UnixDomainSocket> socket_;
+
+    /// @brief Path to the unix domain socket descriptor.
+    ///
+    /// This is used to remove the socket file once the connection terminates.
+    std::string socket_name_;
+
+    /// @brief Pool of connections.
+    ConnectionPool connection_pool_;
+
+    /// @brief Connection timeout
+    long timeout_;
+};
+
+void
+UnixCommandMgrImpl::openCommandSocket(const isc::data::ConstElementPtr& socket_info) {
+    socket_name_.clear();
+
+    if(!socket_info) {
+        isc_throw(BadSocketInfo, "Missing socket_info parameters, can't create socket.");
+    }
+
+    ConstElementPtr type = socket_info->get("socket-type");
+    if (!type) {
+        isc_throw(BadSocketInfo, "Mandatory 'socket-type' parameter missing");
+    }
+
+    // Only supporting unix sockets right now.
+    if (type->stringValue() != "unix") {
+        isc_throw(BadSocketInfo, "Invalid 'socket-type' parameter value "
+                  << type->stringValue());
+    }
+
+    // UNIX socket is requested. It takes one parameter: socket-name that
+    // specifies UNIX path of the socket.
+    ConstElementPtr name = socket_info->get("socket-name");
+    if (!name) {
+        isc_throw(BadSocketInfo, "Mandatory 'socket-name' parameter missing");
+    }
+
+    if (name->getType() != Element::string) {
+        isc_throw(BadSocketInfo, "'socket-name' parameter expected to be a string");
+    }
+
+    socket_name_ = name->stringValue();
+
+    // First let's open lock file.
+    std::string lock_name = getLockName();
+    int lock_fd = open(lock_name.c_str(), O_RDONLY | O_CREAT, 0600);
+    if (lock_fd == -1) {
+        std::string errmsg = strerror(errno);
+        isc_throw(SocketError, "cannot create socket lockfile, "
+                  << lock_name  << ", : " << errmsg);
+    }
+
+    // Try to acquire lock. If we can't somebody else is actively
+    // using it.
+    int ret = flock(lock_fd, LOCK_EX | LOCK_NB);
+    if (ret != 0) {
+        std::string errmsg = strerror(errno);
+        isc_throw(SocketError, "cannot lock socket lockfile, "
+                  << lock_name  << ", : " << errmsg);
+    }
+
+    // We have the lock, so let's remove the pre-existing socket
+    // file if it exists.
+    static_cast<void>(::remove(socket_name_.c_str()));
+
+    LOG_INFO(command_logger, COMMAND_ACCEPTOR_START)
+        .arg(socket_name_);
+
+    try {
+        // Start asynchronous acceptor service.
+        acceptor_.reset(new UnixDomainSocketAcceptor(io_service_));
+        UnixDomainSocketEndpoint endpoint(socket_name_);
+        acceptor_->open(endpoint);
+        acceptor_->bind(endpoint);
+        acceptor_->listen();
+        // Install this socket in Interface Manager.
+        isc::dhcp::IfaceMgr::instance().addExternalSocket(acceptor_->getNative(), 0);
+
+        doAccept();
+
+    } catch (const std::exception& ex) {
+        isc_throw(SocketError, ex.what());
+    }
+}
+
+void
+UnixCommandMgrImpl::doAccept() {
+    // Create a socket into which the acceptor will accept new connection.
+    socket_.reset(new UnixDomainSocket(io_service_));
+    acceptor_->asyncAccept(*socket_, [this](const boost::system::error_code& ec) {
+        if (!ec) {
+            // New connection is arriving. Start asynchronous transmission.
+            ConnectionPtr connection(new Connection(io_service_, socket_,
+                                                    connection_pool_,
+                                                    timeout_));
+            connection_pool_.start(connection);
+
+        } else if (ec.value() != boost::asio::error::operation_aborted) {
+            LOG_ERROR(command_logger, COMMAND_SOCKET_ACCEPT_FAIL)
+                .arg(acceptor_->getNative()).arg(ec.message());
+        }
+
+        // Unless we're stopping the service, start accepting connections again.
+        if (ec.value() != boost::asio::error::operation_aborted) {
+            doAccept();
+        }
+    });
+}
+
+UnixCommandMgr::UnixCommandMgr() : impl_(new UnixCommandMgrImpl()) {
+}
+
+void
+UnixCommandMgr::openCommandSocket(const isc::data::ConstElementPtr& socket_info) {
+    impl_->openCommandSocket(socket_info);
+}
+
+void
+UnixCommandMgr::closeCommandSocket() {
+    // Close acceptor if the acceptor is open.
+    if (impl_->acceptor_ && impl_->acceptor_->isOpen()) {
+        isc::dhcp::IfaceMgr::instance().deleteExternalSocket(impl_->acceptor_->getNative());
+        impl_->acceptor_->close();
+        static_cast<void>(::remove(impl_->socket_name_.c_str()));
+        static_cast<void>(::remove(impl_->getLockName().c_str()));
+    }
+
+    // Stop all connections which can be closed. The only connection that won't
+    // be closed is the one over which we have received a request to reconfigure
+    // the server. This connection will be held until the UnixCommandMgr
+    // responds to such request.
+    impl_->connection_pool_.stopAll();
+}
+
+int
+UnixCommandMgr::getControlSocketFD() {
+    return (impl_->acceptor_ ? impl_->acceptor_->getNative() : -1);
+}
+
+UnixCommandMgr&
+UnixCommandMgr::instance() {
+    static UnixCommandMgr cmd_mgr;
+    return (cmd_mgr);
+}
+
+void
+UnixCommandMgr::setIOService(const IOServicePtr& io_service) {
+    impl_->io_service_ = io_service;
+}
+
+void
+UnixCommandMgr::setConnectionTimeout(const long timeout) {
+    impl_->timeout_ = timeout;
+}
+
+} // end of isc::config
+} // end of isc
diff --git a/src/lib/config/unix_command_mgr.h b/src/lib/config/unix_command_mgr.h
new file mode 100644 (file)
index 0000000..1bed1e5
--- /dev/null
@@ -0,0 +1,88 @@
+// Copyright (C) 2015-2024 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 UNIX_COMMAND_MGR_H
+#define UNIX_COMMAND_MGR_H
+
+#include <asiolink/io_service.h>
+#include <cc/data.h>
+#include <exceptions/exceptions.h>
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+
+namespace isc {
+namespace config {
+
+/// @brief An exception indicating that specified socket parameters are invalid
+class BadSocketInfo : public Exception {
+public:
+    BadSocketInfo(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief An exception indicating a problem with socket operation
+class SocketError : public Exception {
+public:
+    SocketError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+class UnixCommandMgrImpl;
+
+/// @brief Unix Commands Manager implementation for the Kea servers.
+///
+/// This class receives and responds to commands over unix domain sockets.
+class UnixCommandMgr : public boost::noncopyable {
+public:
+
+    /// @brief UnixCommandMgr is a singleton class. This method
+    /// returns reference to its sole instance.
+    ///
+    /// @return the only existing instance of the manager.
+    static UnixCommandMgr& instance();
+
+    /// @brief Sets IO service to be used by the unix command manager.
+    ///
+    /// The server should use this method to provide the Unix Command
+    /// Manager with the common IO service used by the server.
+    /// @param io_service Pointer to the IO service.
+    void setIOService(const asiolink::IOServicePtr& io_service);
+
+    /// @brief Override default connection timeout.
+    ///
+    /// @param timeout New connection timeout in milliseconds.
+    void setConnectionTimeout(const long timeout);
+
+    /// @brief Opens unix control socket with parameters specified in socket_info
+    /// (required parameters: socket-type: unix, socket-name:/unix/path).
+    ///
+    /// @throw BadSocketInfo When socket configuration is invalid.
+    /// @throw SocketError When socket operation fails.
+    ///
+    /// @param socket_info Configuration information for the unix control socket.
+    void
+    openCommandSocket(const isc::data::ConstElementPtr& socket_info);
+
+    /// @brief Shuts down any open unix control sockets
+    void closeCommandSocket();
+
+    /// @brief Returns unix control socket descriptor
+    ///
+    /// This method should be used only in tests.
+    int getControlSocketFD();
+
+private:
+
+    /// @brief Private constructor.
+    UnixCommandMgr();
+
+    /// @brief Pointer to the implementation of the @ref UnixCommandMgr.
+    boost::shared_ptr<UnixCommandMgrImpl> impl_;
+};
+
+} // end of isc::config namespace
+} // end of isc namespace
+#endif