-// Copyright (C) 2021 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2021-2022 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
#include <config/cmd_response_creator.h>
#include <config/command_mgr.h>
-#include <cc/data.h>
+#include <config/config_log.h>
+#include <cc/command_interpreter.h>
#include <http/post_request_json.h>
#include <http/response_json.h>
#include <boost/pointer_cast.hpp>
#include <iostream>
+using namespace isc::config;
using namespace isc::data;
using namespace isc::http;
+using namespace std;
namespace isc {
namespace config {
HttpAuthConfigPtr CmdResponseCreator::http_auth_config_;
+unordered_set<string> CmdResponseCreator::command_accept_list_;
+
HttpRequestPtr
CmdResponseCreator::createNewHttpRequest() const {
return (HttpRequestPtr(new PostHttpRequestJson()));
}
HttpResponsePtr
-CmdResponseCreator::
-createDynamicHttpResponse(HttpRequestPtr request) {
+CmdResponseCreator::createDynamicHttpResponse(HttpRequestPtr request) {
HttpResponseJsonPtr http_response;
// Check the basic HTTP authentication.
// to getBodyAsJson must not trigger an exception.
ConstElementPtr command = request_json->getBodyAsJson();
+ // Filter the command.
+ http_response = filterCommand(request, command, command_accept_list_);
+ if (http_response) {
+ return (http_response);
+ }
+
// Process command doesn't generate exceptions but can possibly return
// null response, if the handler is not implemented properly. This is
// again an internal server issue.
return (http_response);
}
+HttpResponseJsonPtr
+CmdResponseCreator::filterCommand(const HttpRequestPtr& request,
+ const ConstElementPtr& body,
+ const unordered_set<string>& accept) {
+ HttpResponseJsonPtr response;
+ if (!body || accept.empty()) {
+ return (response);
+ }
+ if (body->getType() != Element::map) {
+ return (response);
+ }
+ ConstElementPtr elem = body->get(CONTROL_COMMAND);
+ if (!elem || (elem->getType() != Element::string)) {
+ return (response);
+ }
+ string command = elem->stringValue();
+ if (command.empty() || accept.count(command)) {
+ return (response);
+ }
+
+ // Reject the command.
+ LOG_DEBUG(command_logger, DBG_COMMAND,
+ COMMAND_HTTP_LISTENER_COMMAND_REJECTED)
+ .arg(command)
+ .arg(request->getRemote());
+ // From CtrlAgentResponseCreator::createStockHttpResponseInternal.
+ HttpVersion http_version(request->context()->http_version_major_,
+ request->context()->http_version_minor_);
+ if ((http_version < HttpVersion(1, 0)) ||
+ (HttpVersion(1, 1) < http_version)) {
+ http_version.major_ = 1;
+ http_version.minor_ = 0;
+ }
+ HttpStatusCode status_code = HttpStatusCode::FORBIDDEN;
+ response.reset(new HttpResponseJson(http_version, status_code));
+ ElementPtr response_body = Element::createMap();
+ uint16_t result = HttpResponse::statusCodeToNumber(status_code);
+ response_body->set(CONTROL_RESULT,
+ Element::create(static_cast<long long>(result)));
+ const string& text = HttpResponse::statusCodeToString(status_code);
+ response_body->set(CONTROL_TEXT, Element::create(text));
+ response->setBodyAsJson(response_body);
+ response->finalize();
+ return (response);
+}
+
} // end of namespace isc::config
} // end of namespace isc
-// Copyright (C) 2021 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2021-2022 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
using namespace isc::config;
using namespace isc::data;
using namespace isc::http;
+using namespace std;
namespace ph = std::placeholders;
namespace {
config::CommandMgr::instance().
registerCommand("foo", std::bind(&CmdResponseCreatorTest::fooCommandHandler,
this, ph::_1, ph::_2));
+ // Clear class variables.
+ CmdResponseCreator::http_auth_config_.reset();
+ CmdResponseCreator::command_accept_list_.clear();
}
/// @brief Destructor.
/// Removes registered commands from the command manager.
virtual ~CmdResponseCreatorTest() {
config::CommandMgr::instance().deregisterAll();
+ CmdResponseCreator::http_auth_config_.reset();
+ CmdResponseCreator::command_accept_list_.clear();
}
/// @brief SetUp function that wraps call to initCreator.
/// @param must_contain Text that must be present in the textual
/// representation of the generated response.
void testStockResponse(const HttpStatusCode& status_code,
- const std::string& must_contain) {
+ const string& must_contain) {
HttpResponsePtr response;
ASSERT_NO_THROW(
response = response_creator_->createStockHttpResponse(request_,
HttpResponseJson>(response);
ASSERT_TRUE(response_json);
// Make sure the response contains the string specified as argument.
- EXPECT_TRUE(response_json->toString().find(must_contain) != std::string::npos);
+ EXPECT_TRUE(response_json->toString().find(must_contain) != string::npos);
}
/// @param command_arguments Command arguments (empty).
///
/// @return Returns response with a single string "bar".
- ConstElementPtr fooCommandHandler(const std::string& /*command_name*/,
+ ConstElementPtr fooCommandHandler(const string& /*command_name*/,
const ConstElementPtr& /*command_arguments*/) {
ElementPtr arguments = Element::createList();
arguments->add(Element::create("bar"));
// Response must be successful.
EXPECT_TRUE(response_json->toString().find("HTTP/1.1 200 OK") !=
- std::string::npos);
+ string::npos);
// Response must contain JSON body with "result" of 0.
EXPECT_TRUE(response_json->toString().find("\"result\": 0") !=
- std::string::npos);
+ string::npos);
}
// Test successful server response without emulating agent response.
// Response must be successful.
EXPECT_TRUE(response_json->toString().find("HTTP/1.1 200 OK") !=
- std::string::npos);
+ string::npos);
// Response must contain JSON body with "result" of 0.
EXPECT_TRUE(response_json->toString().find("\"result\": 0") !=
- std::string::npos);
+ string::npos);
}
// This test verifies that Internal Server Error is returned when invalid C++
// Response must contain Internal Server Error status code.
EXPECT_TRUE(response_json->toString().find("HTTP/1.1 500 Internal Server Error") !=
- std::string::npos);
+ string::npos);
+}
+
+// This test verifies command filtering.
+TEST_F(CmdResponseCreatorTest, filterCommand) {
+ initCreator(false);
+ setBasicContext(request_);
+ // For the log message...
+ request_->setRemote("127.0.0.1");
+
+ HttpResponseJsonPtr response;
+ ConstElementPtr body;
+ unordered_set<string> accept;
+ ASSERT_NO_THROW(response = response_creator_->filterCommand(request_, body, accept));
+ EXPECT_FALSE(response);
+
+ accept.insert("foo");
+ ASSERT_NO_THROW(response = response_creator_->filterCommand(request_, body, accept));
+ EXPECT_FALSE(response);
+
+ body = Element::createList();
+ ASSERT_NO_THROW(response = response_creator_->filterCommand(request_, body, accept));
+ EXPECT_FALSE(response);
+
+ body = Element::createMap();
+ ASSERT_NO_THROW(response = response_creator_->filterCommand(request_, body, accept));
+ EXPECT_FALSE(response);
+
+ body = createCommand("foo", ConstElementPtr());
+ ASSERT_NO_THROW(response = response_creator_->filterCommand(request_, body, accept));
+ EXPECT_FALSE(response);
+
+ body = createCommand("bar", ConstElementPtr());
+ ASSERT_NO_THROW(response = response_creator_->filterCommand(request_, body, accept));
+ EXPECT_TRUE(response);
+ EXPECT_EQ("HTTP/1.1 403 Forbidden", response->toBriefString());
+
+ accept.clear();
+ ASSERT_NO_THROW(response = response_creator_->filterCommand(request_, body, accept));
+ EXPECT_FALSE(response);
+}
+
+// This test verifies basic HTTP authentication - reject case.
+// Empty case was handled in createDynamicHttpResponseNoEmulation.
+TEST_F(CmdResponseCreatorTest, basicAuthReject) {
+ initCreator(false);
+ setBasicContext(request_);
+
+ // Body: "foo" command has been registered in the test fixture constructor.
+ request_->context()->body_ = "{ \"command\": \"foo\" }";
+
+ // Add no basic HTTP authentication.
+
+ // All requests must be finalized before they can be processed.
+ ASSERT_NO_THROW(request_->finalize());
+
+ // Create basic HTTP authentication configuration.
+ CmdResponseCreator::http_auth_config_.reset(new BasicHttpAuthConfig());
+ BasicHttpAuthConfigPtr basic =
+ boost::dynamic_pointer_cast<BasicHttpAuthConfig>(
+ CmdResponseCreator::http_auth_config_);
+ ASSERT_TRUE(basic);
+ EXPECT_NO_THROW(basic->add("test", "", "123\xa3", ""));
+
+ // Create response from the request.
+ HttpResponsePtr response;
+ ASSERT_NO_THROW(response = response_creator_->createHttpResponse(request_));
+ ASSERT_TRUE(response);
+
+ // Response must not be successful.
+ EXPECT_EQ("HTTP/1.1 401 Unauthorized", response->toBriefString());
+}
+
+// This test verifies basic HTTP authentication - accept case.
+// Empty case was handled in createDynamicHttpResponseNoEmulation.
+TEST_F(CmdResponseCreatorTest, basicAuthAccept) {
+ initCreator(false);
+ setBasicContext(request_);
+
+ // Body: "foo" command has been registered in the test fixture constructor.
+ request_->context()->body_ = "{ \"command\": \"foo\" }";
+
+ // Add basic HTTP authentication.
+ HttpHeaderContext auth("Authorization", "Basic dGVzdDoxMjPCow==");
+ request_->context()->headers_.push_back(auth);
+
+ // All requests must be finalized before they can be processed.
+ ASSERT_NO_THROW(request_->finalize());
+
+ // Create basic HTTP authentication configuration.
+ CmdResponseCreator::http_auth_config_.reset(new BasicHttpAuthConfig());
+ BasicHttpAuthConfigPtr basic =
+ boost::dynamic_pointer_cast<BasicHttpAuthConfig>(
+ CmdResponseCreator::http_auth_config_);
+ ASSERT_TRUE(basic);
+ EXPECT_NO_THROW(basic->add("test", "", "123\xa3", ""));
+
+ // Create response from the request.
+ HttpResponsePtr response;
+ ASSERT_NO_THROW(response = response_creator_->createHttpResponse(request_));
+ ASSERT_TRUE(response);
+
+ // Response must be convertible to HttpResponseJsonPtr.
+ HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
+ HttpResponseJson>(response);
+ ASSERT_TRUE(response_json);
+
+ // Response must be successful.
+ EXPECT_TRUE(response_json->toString().find("HTTP/1.1 200 OK") !=
+ string::npos);
+
+ // Response must contain JSON body with "result" of 0.
+ EXPECT_TRUE(response_json->toString().find("\"result\": 0") !=
+ string::npos);
+}
+
+// This test verifies command filtering at the HTTP level - reject case.
+TEST_F(CmdResponseCreatorTest, filterCommandReject) {
+ initCreator(false);
+ setBasicContext(request_);
+ // For the log message...
+ request_->setRemote("127.0.0.1");
+
+ // Body: "bar" command has been registered in the test fixture constructor.
+ request_->context()->body_ = "{ \"command\": \"bar\" }";
+
+ // All requests must be finalized before they can be processed.
+ ASSERT_NO_THROW(request_->finalize());
+
+ // Add foo in the access list.
+ CmdResponseCreator::command_accept_list_.insert("foo");
+
+ // Create response from the request.
+ HttpResponsePtr response;
+ ASSERT_NO_THROW(response = response_creator_->createHttpResponse(request_));
+ ASSERT_TRUE(response);
+
+ // Response must not be successful.
+ EXPECT_EQ("HTTP/1.1 403 Forbidden", response->toBriefString());
+}
+
+// This test verifies command filtering at the HTTP level - accept case.
+TEST_F(CmdResponseCreatorTest, filterCommandAccept) {
+ initCreator(false);
+ setBasicContext(request_);
+
+ // Body: "foo" command has been registered in the test fixture constructor.
+ request_->context()->body_ = "{ \"command\": \"foo\" }";
+
+ // All requests must be finalized before they can be processed.
+ ASSERT_NO_THROW(request_->finalize());
+
+ // Add foo in the access list.
+ CmdResponseCreator::command_accept_list_.insert("foo");
+
+ // Create response from the request.
+ HttpResponsePtr response;
+ ASSERT_NO_THROW(response = response_creator_->createHttpResponse(request_));
+ ASSERT_TRUE(response);
+
+ // Response must be convertible to HttpResponseJsonPtr.
+ HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
+ HttpResponseJson>(response);
+ ASSERT_TRUE(response_json);
+
+ // Response must be successful.
+ EXPECT_TRUE(response_json->toString().find("HTTP/1.1 200 OK") !=
+ string::npos);
+
+ // Response must contain JSON body with "result" of 0.
+ EXPECT_TRUE(response_json->toString().find("\"result\": 0") !=
+ string::npos);
}
}