return (createAnswer(0, config));
}
+ConstElementPtr
+ControlledDhcpv4Srv::commandWriteConfigHandler(const string&,
+ ConstElementPtr args) {
+ ConstElementPtr config = CfgMgr::instance().getCurrentCfg()->toElement();
+
+ string filename;
+
+ if (args) {
+ if (args->getType() != Element::map) {
+ return (createAnswer(CONTROL_RESULT_ERROR, "Argument must be a map"));
+ }
+ ConstElementPtr filename_param = args->get("filename");
+ if (filename_param) {
+ if (filename_param->getType() != Element::string) {
+ return (createAnswer(CONTROL_RESULT_ERROR,
+ "passed parameter 'filename' is not a string"));
+ }
+ filename = filename_param->stringValue();
+ }
+ }
+
+ if (filename.empty()) {
+ // filename parameter was not specified, so let's use whatever we remember
+ filename = getConfigFile();
+ if (filename.empty()) {
+ return (createAnswer(CONTROL_RESULT_ERROR, "Unable to determine filename."
+ "Please specify filename explicitly."));
+ }
+ }
+
+ // Now do the sanity checks on the filename
+ if (filename.find("..") != string::npos) {
+ // Trying to escape the directory.. nope.
+ return (createAnswer(CONTROL_RESULT_ERROR,
+ "Using '..' in filename is not allowed."));
+ }
+
+ if (filename.find("\\") != string::npos) {
+ // Trying to inject escapes (possibly to inject quotes and something
+ // nasty afterward)
+ return (createAnswer(CONTROL_RESULT_ERROR,
+ "Using \\ in filename is not allowed."));
+ }
+
+ if (filename[0] == '/') {
+ // Absolute paths are not allowed.
+ return (createAnswer(CONTROL_RESULT_ERROR,
+ "Absolute path in filename is not allowed."));
+ }
+
+ size_t size = 0;
+ try {
+ size = writeConfigFile(filename);
+ } catch (const isc::Exception& ex) {
+ return (createAnswer(CONTROL_RESULT_ERROR, string("Error during write-config:")
+ + ex.what()));
+ }
+ if (size == 0) {
+ return (createAnswer(CONTROL_RESULT_ERROR, "Error writing configuration to "
+ + filename));
+ }
+
+ // Ok, it's time to return the successful response
+ ElementPtr params = Element::createMap();
+ params->set("size", Element::create(static_cast<long long>(size)));
+ params->set("filename", Element::create(filename));
+
+ return (createAnswer(CONTROL_RESULT_SUCCESS, "Configuration written to "
+ + filename + " successful", params));
+}
+
ConstElementPtr
ControlledDhcpv4Srv::commandSetConfigHandler(const string&,
ConstElementPtr args) {
} else if (command == "leases-reclaim") {
return (srv->commandLeasesReclaimHandler(command, args));
+ } else if (command == "write-config") {
+ return (srv->commandWriteConfigHandler(command, args));
}
ConstElementPtr answer = isc::config::createAnswer(1,
"Unrecognized command:" + command);
CommandMgr::instance().registerCommand("statistic-remove-all",
boost::bind(&StatsMgr::statisticRemoveAllHandler, _1, _2));
+
+ CommandMgr::instance().registerCommand("write-config",
+ boost::bind(&ControlledDhcpv4Srv::commandWriteConfigHandler, this, _1, _2));
}
void ControlledDhcpv4Srv::shutdown() {
CommandMgr::instance().deregisterCommand("statistic-get-all");
CommandMgr::instance().deregisterCommand("statistic-reset-all");
CommandMgr::instance().deregisterCommand("statistic-remove-all");
+ CommandMgr::instance().deregisterCommand("write-config");
} catch (...) {
// Don't want to throw exceptions from the destructor. The server
commandConfigReloadHandler(const std::string& command,
isc::data::ConstElementPtr args);
+ /// @brief handler for processing 'get-config' command
+ ///
+ /// This handler processes get-config command, which retrieves
+ /// the current configuration and returns it in response.
+ ///
+ /// @param command (ignored)
+ /// @param args (ignored)
+ /// @return current configuration wrapped in a response
isc::data::ConstElementPtr
commandGetConfigHandler(const std::string& command,
isc::data::ConstElementPtr args);
+ /// @brief handler for processing 'write-config' command
+ ///
+ /// This handle processes write-config comamnd, which writes the
+ /// current configuration to disk. This command takes one optional
+ /// parameter called filename. If specified, the current configuration
+ /// will be written to that file. If not specified, the file used during
+ /// Kea start-up will be used. The filename must be within the
+ /// {prefix} directory specified during Kea compilation. This is
+ /// a security measure against exploiting file writes remotely.
+ ///
+ /// @param command (ignored)
+ /// @param args may contain optional string argument filename
+ /// @return status of the configuration file write
+ isc::data::ConstElementPtr
+ commandWriteConfigHandler(const std::string& command,
+ isc::data::ConstElementPtr args);
+
/// @brief handler for processing 'set-config' command
///
/// This handler processes set-config command, which processes
client->disconnectFromServer();
ASSERT_NO_THROW(server_->receivePacket(0));
}
+
+ /// @brief Checks response for list-commands
+ ///
+ /// This method checks if the list-commands response is generally sane
+ /// and whether specified command is mentioned in the response.
+ ///
+ /// @param rsp response sent back by the server
+ /// @param command command expected to be on the list.
+ void checkListCommands(const ConstElementPtr& rsp, const std::string& command) {
+ ConstElementPtr params;
+ int status_code;
+ EXPECT_NO_THROW(params = parseAnswer(status_code, rsp));
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status_code);
+ ASSERT_TRUE(params);
+ ASSERT_EQ(Element::list, params->getType());
+
+ int cnt = 0;
+ for (int i=0; i < params->size(); ++i) {
+ string tmp = params->get(i)->stringValue();
+ if (tmp == command) {
+ // Command found, but that's not enough. Need to continue working
+ // through the list to see if there are no duplicates.
+ cnt++;
+ }
+ }
+
+ // Exactly one command on the list is expected.
+ EXPECT_EQ(1, cnt) << "Command " << command << " not found";
+ }
+
+ /// @brief Check if the answer for write-config command is correct
+ ///
+ /// @param response_txt response in text form (as read from the control socket)
+ /// @param exp_status expected status (0 success, 1 failure)
+ /// @param exp_txt for success cases this defines the expected filename,
+ /// for failure cases this defines the expected error message
+ void checkWriteConfig(const std::string& response_txt, int exp_status,
+ const std::string& exp_txt = "") {
+
+ cout << "#### response=" << response_txt << endl;
+
+ ConstElementPtr rsp;
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response_txt));
+ ASSERT_TRUE(rsp);
+
+ int status;
+ ConstElementPtr params = parseAnswer(status, rsp);
+ EXPECT_EQ(exp_status, status);
+
+ if (exp_status == CONTROL_RESULT_SUCCESS) {
+ // Let's check couple things...
+
+ // The parameters must include filename
+ ASSERT_TRUE(params);
+ ASSERT_TRUE(params->get("filename"));
+ EXPECT_EQ(Element::string, params->get("filename")->getType());
+ EXPECT_EQ(exp_txt, params->get("filename")->stringValue());
+
+ // The parameters must include size. And the size
+ // must indicate some content.
+ ASSERT_TRUE(params->get("size"));
+ EXPECT_EQ(Element::integer, params->get("size")->getType());
+ int64_t size = params->get("size")->intValue();
+ EXPECT_LE(1, size);
+
+ // Now check if the file is really there and suitable for
+ // opening.
+ ifstream f(exp_txt, ios::binary | ios::ate);
+ ASSERT_TRUE(f.good());
+
+ // Now check that it is the correct size as reported.
+ EXPECT_EQ(size, static_cast<int64_t>(f.tellg()));
+
+ // Finally, check that it's really a JSON.
+ ElementPtr from_file = Element::fromJSONFile(exp_txt);
+ ASSERT_TRUE(from_file);
+ } else if (exp_status == CONTROL_RESULT_ERROR) {
+
+ // Let's check if the reason for failure was given.
+ ConstElementPtr text = rsp->get("text");
+ ASSERT_TRUE(text);
+ ASSERT_EQ(Element::string, text->getType());
+ EXPECT_EQ(exp_txt, text->stringValue());
+ } else {
+ ADD_FAILURE() << "Invalid expected status: " << exp_status;
+ }
+ }
};
TEST_F(CtrlChannelDhcpv4SrvTest, commands) {
CfgMgr::instance().clear();
}
+// Tests that the server properly responds to shtudown command sent
+// via ControlChannel
+TEST_F(CtrlChannelDhcpv4SrvTest, listCommands) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"list-commands\" }", response);
+
+ ConstElementPtr rsp;
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+
+ // We expect the server to report at least the following commands:
+ checkListCommands(rsp, "get-config");
+ checkListCommands(rsp, "list-commands");
+ checkListCommands(rsp, "leases-reclaim");
+ checkListCommands(rsp, "libreload");
+ checkListCommands(rsp, "set-config");
+ checkListCommands(rsp, "shutdown");
+ checkListCommands(rsp, "statistic-get");
+ checkListCommands(rsp, "statistic-get-all");
+ checkListCommands(rsp, "statistic-remove");
+ checkListCommands(rsp, "statistic-remove-all");
+ checkListCommands(rsp, "statistic-reset");
+ checkListCommands(rsp, "statistic-reset-all");
+ checkListCommands(rsp, "write-config");
+}
+
+// Tests if the server returns its configuration using get-config.
+// Note there are separate tests that verify if toElement() called by the
+// get-config handler are actually converting the configuration correctly.
+TEST_F(CtrlChannelDhcpv4SrvTest, getConfig) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"get-config\" }", response);
+ ConstElementPtr rsp;
+
+ // The response should be a valid JSON.
+ EXPECT_NO_THROW(rsp = Element::fromJSON(response));
+ ASSERT_TRUE(rsp);
+
+ int status;
+ ConstElementPtr cfg = parseAnswer(status, rsp);
+ EXPECT_EQ(CONTROL_RESULT_SUCCESS, status);
+
+ // Ok, now roughly check if the response seems legit.
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(Element::map, cfg->getType());
+ EXPECT_TRUE(cfg->get("Dhcp4"));
+}
+
+
+TEST_F(CtrlChannelDhcpv4SrvTest, writeConfigNoFilename) {
+ createUnixChannelServer();
+ std::string response;
+
+ // This is normally set by the command line -c parameter.
+ server_->setConfigFile("test1.json");
+
+ // If the filename is not explicitly specified, the name used
+ // in -c command line switch is used.
+ sendUnixCommand("{ \"command\": \"write-config\" }", response);
+
+ checkWriteConfig(response, CONTROL_RESULT_SUCCESS, "test1.json");
+ ::remove("test1.json");
+}
+
+TEST_F(CtrlChannelDhcpv4SrvTest, writeConfigFilename) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"write-config\", "
+ "\"arguments\": { \"filename\": \"test2.json\" } }", response);
+ checkWriteConfig(response, CONTROL_RESULT_SUCCESS, "test2.json");
+ ::remove("test2.json");
+}
+
+TEST_F(CtrlChannelDhcpv4SrvTest, writeConfigInvalidJailEscape) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"write-config\", \"arguments\": "
+ "{ \"filename\": \"../test3.json\" } }", response);
+ checkWriteConfig(response, CONTROL_RESULT_ERROR,
+ "Using '..' in filename is not allowed.");
+}
+
+TEST_F(CtrlChannelDhcpv4SrvTest, writeConfigInvalidAbsPath) {
+ createUnixChannelServer();
+ std::string response;
+
+ sendUnixCommand("{ \"command\": \"write-config\", \"arguments\": "
+ "{ \"filename\": \"/tmp/test4.json\" } }", response);
+ checkWriteConfig(response, CONTROL_RESULT_ERROR,
+ "Absolute path in filename is not allowed.");
+}
+
+TEST_F(CtrlChannelDhcpv4SrvTest, writeConfigInvalidEscape) {
+ createUnixChannelServer();
+ std::string response;
+
+ // This will be converted to foo(single backslash)test5.json
+ sendUnixCommand("{ \"command\": \"write-config\", \"arguments\": "
+ "{ \"filename\": \"foo\\\\test5.json\" } }", response);
+ checkWriteConfig(response, CONTROL_RESULT_ERROR,
+ "Using \\ in filename is not allowed.");
+}
+
} // End of anonymous namespace