From: Francis Dupont Date: Sun, 1 Nov 2020 16:44:50 +0000 (+0100) Subject: [#1421] Implemented auth and response hook X-Git-Tag: Kea-1.9.2~20 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=51e5cbff04e2f359f044e62e445a6e35a59a0e5c;p=thirdparty%2Fkea.git [#1421] Implemented auth and response hook --- diff --git a/src/bin/agent/agent_hooks.dox b/src/bin/agent/agent_hooks.dox index 20bf584eff..641eb55c27 100644 --- a/src/bin/agent/agent_hooks.dox +++ b/src/bin/agent/agent_hooks.dox @@ -1,4 +1,4 @@ -// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2017-2020 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 @@ -23,36 +23,34 @@ command. @section agentHooksHookPoints Hooks in the Control Agent - @subsection agentHooksControlCommandReceive control_command_receive + @subsection agentHooksAuth auth - @b Arguments: - - name: @b command, type: isc::data::ConstElementPtr, direction: in/out - - name: @b response, type: isc::data::ConstElementPtr, direction: in/out + - name: @b request, type: isc::http::HttpRequestPtr, direction: in/out + - name: @b response, type: isc::http::HttpResponseJsonPtr, direction: in/out - @b Description: this callout is executed when Control Agent receives a control command over the RESTful interface (HTTP). - The "command" argument is a pointer to the parsed JSON structure - including command name and command arguments. If the callout implements - the specified command, it handles the command and creates appropriate - response. The response should be returned in the "response" argument. - In most cases, the callout which handles the command will set the next - step action to SKIP, to prevent the server from trying to handle the - command on its own and overriding the response created by the callouts. - A notable exception is the 'list-commands' command for which the callouts - should not set the next step action to SKIP. The server has a special - code path for this command which combines the list of commands returned - by the callouts with the list of commands supported by the server. If - the callout sets the next step action to SKIP in this case, the server - will only return the list of commands supported by the hook library. - The callout can modify the command arguments to influence the command - processing by the Command Manager. For example, it may freely modify - the configuration received in 'config-set' before it is processed by - the server. The SKIP action is not set in this case. - - - Next step status: if any callout sets the next step action to SKIP, - the server will assume that the command has been handled by the callouts - and will expect that the response is provided in the "response" argument. - The Control Agent will not handle the command in this case but simply - return the response returned by the callout to the caller. + The "request" argument is a pointer to the request, in fact a + PostHttpRequestJsonPtr. The "response" argument is the response in + case of errors. The purpose of this callout is to implement authentication + and authorization. It is called after basic HTTP authentication. + The next step status is ignored: if the response is set the processing + will stop and the response is returned. In particular the command is not + forwarded. + + @subsection agentHooksResponse response + + - @b Arguments: + - name: @b request, type: isc::http::HttpRequestPtr, direction: in + - name: @b response, type: isc::http::HttpResponseJsonPtr, direction: in/out + + - @b Description: this callout is executed when Control Agent executed + a control command over the RESTful interface (HTTP). + The "request" argument is a pointer to the request. It is used as a + reference and for callout contexts. The "response" argument is the + response which will be sent back to the resquesting client. It is + called after command processing. The next step status is ignored: + the response eventually modified will be sent back. */ \ No newline at end of file diff --git a/src/bin/agent/ca_response_creator.cc b/src/bin/agent/ca_response_creator.cc index 702d031688..9bfdec3ef5 100644 --- a/src/bin/agent/ca_response_creator.cc +++ b/src/bin/agent/ca_response_creator.cc @@ -12,14 +12,40 @@ #include #include #include +#include +#include +#include #include #include #include #include using namespace isc::data; +using namespace isc::hooks; using namespace isc::http; +namespace { + +/// Structure that holds registered hook indexes. +struct CtrlAgentHooks { + int hook_index_auth_; ///< index of "auth" hook point. + int hook_index_response_; ///< index of "response" hook point. + + /// Constructor that registers hook points. + CtrlAgentHooks() { + hook_index_auth_ = HooksManager::registerHook("auth"); + hook_index_response_ = HooksManager::registerHook("response"); + } +}; + +} // end of anonymous namespace. + +// Declare a Hooks object. As this is outside any function or method, it +// will be instantiated (and the constructor run) when the module is loaded. +// As a result, the hook indexes will be defined before any method in this +// module is called. +CtrlAgentHooks Hooks; + namespace isc { namespace agent { @@ -30,7 +56,7 @@ CtrlAgentResponseCreator::createNewHttpRequest() const { HttpResponsePtr CtrlAgentResponseCreator:: -createStockHttpResponse(const ConstHttpRequestPtr& request, +createStockHttpResponse(const HttpRequestPtr& request, const HttpStatusCode& status_code) const { HttpResponsePtr response = createStockHttpResponseInternal(request, status_code); response->finalize(); @@ -39,7 +65,7 @@ createStockHttpResponse(const ConstHttpRequestPtr& request, HttpResponsePtr CtrlAgentResponseCreator:: -createStockHttpResponseInternal(const ConstHttpRequestPtr& request, +createStockHttpResponseInternal(const HttpRequestPtr& request, const HttpStatusCode& status_code) const { // The request hasn't been finalized so the request object // doesn't contain any information about the HTTP version number @@ -60,7 +86,7 @@ createStockHttpResponseInternal(const ConstHttpRequestPtr& request, HttpResponsePtr CtrlAgentResponseCreator:: -createDynamicHttpResponse(const ConstHttpRequestPtr& request) { +createDynamicHttpResponse(HttpRequestPtr request) { // First check authentication. HttpResponseJsonPtr http_response; @@ -89,19 +115,39 @@ createDynamicHttpResponse(const ConstHttpRequestPtr& request) { } } } - // The basic HTTP authentication check failed and left a response. + + // Callout point for "auth". + if (HooksManager::calloutsPresent(Hooks.hook_index_auth_)) { + // Get callout handle. + CalloutHandlePtr callout_handle = request->getCalloutHandle(); + ScopedCalloutHandleState callout_handle_state(callout_handle); + + // Pass arguments. + callout_handle->setArgument("request", request); + callout_handle->setArgument("response", http_response); + + // Call callouts. + HooksManager::callCallouts(Hooks.hook_index_auth_, *callout_handle); + callout_handle->getArgument("request", request); + callout_handle->getArgument("response", http_response); + + // Ignore status as the HTTP response is used instead. + } + + // The basic HTTP authentication check or a callout failed and + // left a response. if (http_response) { return (http_response); } // The request is always non-null, because this is verified by the // createHttpResponse method. Let's try to convert it to the - // ConstPostHttpRequestJson type as this is the type generated by the + // PostHttpRequestJson type as this is the type generated by the // createNewHttpRequest. If the conversion result is null it means that // the caller did not use createNewHttpRequest method to create this // instance. This is considered an error in the server logic. - ConstPostHttpRequestJsonPtr request_json = boost::dynamic_pointer_cast< - const PostHttpRequestJson>(request); + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast(request); if (!request_json) { // Notify the client that we have a problem with our server. return (createStockHttpResponse(request, HttpStatusCode::INTERNAL_SERVER_ERROR)); @@ -125,6 +171,24 @@ createDynamicHttpResponse(const ConstHttpRequestPtr& request) { http_response->setBodyAsJson(response); http_response->finalize(); + // Callout point for "response". + if (HooksManager::calloutsPresent(Hooks.hook_index_response_)) { + // Get callout handle. + CalloutHandlePtr callout_handle = request->getCalloutHandle(); + ScopedCalloutHandleState callout_handle_state(callout_handle); + + // Pass arguments. + callout_handle->setArgument("request", request); + callout_handle->setArgument("response", http_response); + + // Call callouts. + HooksManager::callCallouts(Hooks.hook_index_response_, + *callout_handle); + callout_handle->getArgument("response", http_response); + + // Ignore status as the HTTP response is used instead. + } + return (http_response); } diff --git a/src/bin/agent/ca_response_creator.h b/src/bin/agent/ca_response_creator.h index d8f1dd8ae7..189a323b42 100644 --- a/src/bin/agent/ca_response_creator.h +++ b/src/bin/agent/ca_response_creator.h @@ -1,4 +1,4 @@ -// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2017-2020 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 @@ -52,7 +52,7 @@ public: /// @return Pointer to an @ref isc::http::HttpResponseJson object /// representing stock HTTP response. virtual http::HttpResponsePtr - createStockHttpResponse(const http::ConstHttpRequestPtr& request, + createStockHttpResponse(const http::HttpRequestPtr& request, const http::HttpStatusCode& status_code) const; private: @@ -68,7 +68,7 @@ private: /// @return Pointer to an @ref isc::http::HttpResponseJson object /// representing stock HTTP response. http::HttpResponsePtr - createStockHttpResponseInternal(const http::ConstHttpRequestPtr& request, + createStockHttpResponseInternal(const http::HttpRequestPtr& request, const http::HttpStatusCode& status_code) const; /// @brief Creates implementation specific HTTP response. @@ -76,7 +76,7 @@ private: /// @param request Pointer to an object representing HTTP request. /// @return Pointer to an object representing HTTP response. virtual http::HttpResponsePtr - createDynamicHttpResponse(const http::ConstHttpRequestPtr& request); + createDynamicHttpResponse(http::HttpRequestPtr request); }; } // end of namespace isc::agent diff --git a/src/bin/agent/tests/Makefile.am b/src/bin/agent/tests/Makefile.am index b694b24afa..834b776db8 100644 --- a/src/bin/agent/tests/Makefile.am +++ b/src/bin/agent/tests/Makefile.am @@ -41,7 +41,7 @@ TESTS_ENVIRONMENT = \ TESTS = if HAVE_GTEST -noinst_LTLIBRARIES = libbasic.la +noinst_LTLIBRARIES = libcallout.la libbasicauth.la TESTS += ca_unittests @@ -82,13 +82,27 @@ ca_unittests_LDADD += $(LOG4CPLUS_LIBS) $(CRYPTO_LIBS) ca_unittests_LDADD += $(BOOST_LIBS) $(GTEST_LDADD) # The basic callout library - contains standard callouts -libbasic_la_SOURCES = basic_library.cc -libbasic_la_CXXFLAGS = $(AM_CXXFLAGS) -libbasic_la_CPPFLAGS = $(AM_CPPFLAGS) -libbasic_la_LIBADD = $(top_builddir)/src/lib/exceptions/libkea-exceptions.la -libbasic_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la -libbasic_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la -libbasic_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere +libcallout_la_SOURCES = callout_library.cc +libcallout_la_CXXFLAGS = $(AM_CXXFLAGS) +libcallout_la_CPPFLAGS = $(AM_CPPFLAGS) +libcallout_la_LIBADD = $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +libcallout_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la +libcallout_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la +libcallout_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere + +# The basic HTTP auth as a callout library +libbasicauth_la_SOURCES = basic_auth_library.cc +libbasicauth_la_CXXFLAGS = $(AM_CXXFLAGS) +libbasicauth_la_CPPFLAGS = $(AM_CPPFLAGS) +libbasicauth_la_LIBADD = $(top_builddir)/src/lib/http/libkea-http.la +libbasicauth_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la +libbasicauth_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la +libbasicauth_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la +libbasicauth_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la +libbasicauth_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la +libbasicauth_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +libbasicauth_la_LIBADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS) +libbasicauth_la_LDFLAGS = -avoid-version -export-dynamic -module -rpath /nowhere nodist_ca_unittests_SOURCES = test_data_files_config.h test_libraries.h diff --git a/src/bin/agent/tests/basic_auth_library.cc b/src/bin/agent/tests/basic_auth_library.cc new file mode 100644 index 0000000000..f3581b6e12 --- /dev/null +++ b/src/bin/agent/tests/basic_auth_library.cc @@ -0,0 +1,275 @@ +// Copyright (C) 2020 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/. + +/// @file +/// @brief Basic HTTP Authentication callout library + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace isc::data; +using namespace isc::hooks; +using namespace isc::http; +using namespace isc; +using namespace std; + +namespace { + +/// @brief Response creator. +class ResponseCreator : public HttpResponseCreator { +public: + /// @brief Create a new request. + /// @return Pointer to the new instance of the @ref + /// isc::http::PostHttpRequestJson. + virtual HttpRequestPtr + createNewHttpRequest() const; + + /// @brief Create stock HTTP response. + /// + /// @param request Pointer to an object representing HTTP request. + /// @param status_code Status code of the response. + /// @return Pointer to an @ref isc::http::HttpResponseJson object + /// representing stock HTTP response. + virtual HttpResponsePtr + createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const; + + /// @brief Creates implementation specific HTTP response. + /// + /// @param request Pointer to an object representing HTTP request. + /// @return Pointer to an object representing HTTP response. + virtual HttpResponsePtr + createDynamicHttpResponse(HttpRequestPtr request); +}; + +HttpRequestPtr +ResponseCreator::createNewHttpRequest() const { + return (HttpRequestPtr(new PostHttpRequestJson())); +} + +HttpResponsePtr +ResponseCreator::createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const { + HttpVersion http_version(1, 1); + HttpResponsePtr response(new HttpResponseJson(http_version, status_code)); + response->finalize(); + return (response); +} + +HttpResponsePtr +ResponseCreator::createDynamicHttpResponse(HttpRequestPtr request) { + isc_throw(NotImplemented, "createDynamicHttpResponse should not be called"); +} + +/// @brief The type of shared pointers to response creators. +typedef boost::shared_ptr ResponseCreatorPtr; + +/// @brief Implementation. +class Impl { +public: + + /// @brief Constructor. + Impl(); + + /// @brief Destructor. + ~Impl(); + + /// @brief Configure. + /// + /// @param config element pointer to client list. + void configure(ConstElementPtr config); + + /// @brief Basic HTTP authentication configuration. + BasicHttpAuthConfigPtr config_; + + /// @brief Response creator. + ResponseCreatorPtr creator_; +}; + +Impl::Impl() + : config_(new BasicHttpAuthConfig()), creator_(new ResponseCreator()) { +} + +Impl::~Impl() { +} + +void +Impl::configure(ConstElementPtr config) { + config_->parse(config); +} + +/// @brief The type of shared pointers to implementations. +typedef boost::shared_ptr ImplPtr; + +/// @brief The implementation. +ImplPtr impl; + +extern "C" { + +// Framework functions. + +/// @brief returns Kea hooks version. +int +version() { + return (KEA_HOOKS_VERSION); +} + +/// @brief This function is called when the library is loaded. +/// +/// @param handle library handle. +/// @return 0 when initialization is successful, 1 otherwise. +int +load(LibraryHandle& handle) { +#ifdef USE_STATIC_LINK + hooksStaticLinkInit(); +#endif + try { + impl.reset(new Impl()); + ConstElementPtr config = handle.getParameter("config"); + impl->configure(config); + } catch (const std::exception& ex) { + std::cerr << "load error: " << ex.what() << std::endl; + return (1); + } + + return (0); +} + +/// @brief This function is called when the library is unloaded. +/// +/// @return always 0. +int +unload() { + impl.reset(); + return (0); +} + +// Callout functions. + +/// @brief This callout is called at the "auth" hook. +/// +/// @param handle CalloutHandle. +/// @return 0 upon success, non-zero otherwise. +int +auth(CalloutHandle& handle) { + // Sanity. + if (!impl) { + std::cerr << "no implementation" << std::endl; + return (0); + } + + // Get the parameters. + HttpRequestPtr request; + HttpResponseJsonPtr response; + handle.getArgument("request", request); + handle.getArgument("response", response); + + if (response) { + std::cerr << "response already set" << std::endl; + return (0); + } + if (!request) { + std::cerr << "no request" << std::endl; + return (0); + } + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast(request); + if (!request_json) { + std::cerr << "no json post request" << std::endl; + return (0); + } + ConstElementPtr command = request_json->getBodyAsJson(); + if (!command) { + std::cerr << "no command" << std::endl; + return (0); + } + if (command->getType() != Element::map) { + std::cerr << "command is not a map" << std::endl; + return (0); + } + + // Modify request. + ElementPtr mutable_command = boost::const_pointer_cast(command); + if (command->contains("service")) { + mutable_command->remove("service"); + request_json->setBodyAsJson(command); + } + + // Perform authentication. + response = impl->config_->checkAuth(*impl->creator_, request); + + // Set parameters. + handle.setArgument("request", request); + handle.setArgument("response", response); + return (0); +} + +/// @brief This callout is called at the "response" hook. +/// +/// @param handle CalloutHandle. +/// @return 0 upon success, non-zero otherwise. +int +response(CalloutHandle& handle) { + // Sanity. + if (!impl) { + std::cerr << "no implementation" << std::endl; + return (0); + } + + // Get the parameters. + HttpRequestPtr request; + HttpResponseJsonPtr response; + handle.getArgument("request", request); + handle.getArgument("response", response); + + if (!request) { + std::cerr << "no request" << std::endl; + return (0); + } + if (!response) { + std::cerr << "no response" << std::endl; + return (0); + } + + // Modify response. + ConstElementPtr body = response->getBodyAsJson(); + if (!body) { + std::cerr << "no body" << std::endl; + return (0); + } + if (body->getType() != Element::list) { + std::cerr << "body is not a list" << std::endl; + return (0); + } + if (body->size() < 1) { + std::cerr << "body is empty" << std::endl; + return (0); + } + ConstElementPtr answer = body->get(0); + if (!answer || (answer->getType() != Element::map)) { + std::cerr << "answer is not map" << std::endl; + return (0); + } + ElementPtr mutable_answer = boost::const_pointer_cast(answer); + mutable_answer->set("comment", Element::create(string("got"))); + response->setBodyAsJson(body); + + // Set parameters. + handle.setArgument("response", response); + return (0); +} + +} +} diff --git a/src/bin/agent/tests/ca_cfg_mgr_unittests.cc b/src/bin/agent/tests/ca_cfg_mgr_unittests.cc index 480e8c2234..4ae7f91fda 100644 --- a/src/bin/agent/tests/ca_cfg_mgr_unittests.cc +++ b/src/bin/agent/tests/ca_cfg_mgr_unittests.cc @@ -470,7 +470,7 @@ TEST_F(AgentParserTest, configParse3Sockets) { // can't use AGENT_CONFIGS[4] as is, but need to run it through path replacer. TEST_F(AgentParserTest, configParseHooks) { // Create the configuration with proper lib path. - std::string cfg = pathReplacer(AGENT_CONFIGS[4], BASIC_CALLOUT_LIBRARY); + std::string cfg = pathReplacer(AGENT_CONFIGS[4], CALLOUT_LIBRARY); // The configuration should be successful. configParse(cfg.c_str(), 0); @@ -478,7 +478,7 @@ TEST_F(AgentParserTest, configParseHooks) { CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext(); const HookLibsCollection libs = ctx->getHooksConfig().get(); ASSERT_EQ(1, libs.size()); - EXPECT_EQ(string(BASIC_CALLOUT_LIBRARY), libs[0].first); + EXPECT_EQ(string(CALLOUT_LIBRARY), libs[0].first); ASSERT_TRUE(libs[0].second); EXPECT_EQ("{ \"param1\": \"foo\" }", libs[0].second->str()); } diff --git a/src/bin/agent/tests/ca_response_creator_unittests.cc b/src/bin/agent/tests/ca_response_creator_unittests.cc index 58bce6800e..c49ab858a9 100644 --- a/src/bin/agent/tests/ca_response_creator_unittests.cc +++ b/src/bin/agent/tests/ca_response_creator_unittests.cc @@ -10,11 +10,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -23,6 +25,7 @@ using namespace isc; using namespace isc::agent; using namespace isc::config; using namespace isc::data; +using namespace isc::hooks; using namespace isc::http; using namespace isc::process; namespace ph = std::placeholders; @@ -68,6 +71,8 @@ public: /// Removes registered commands from the command manager. virtual ~CtrlAgentResponseCreatorTest() { CtrlAgentCommandMgr::instance().deregisterAll(); + HooksManager::prepareUnloadLibraries(); + static_cast(HooksManager::unloadLibraries()); } /// @brief Fills request context with required data to create new request. @@ -315,4 +320,105 @@ TEST_F(CtrlAgentResponseCreatorTest, basicAuth) { std::string::npos); } +// This test verifies that Unauthorized is returned when authentication is +// required but not provided by request using the hook. +TEST_F(CtrlAgentResponseCreatorTest, hookNoAuth) { + setBasicContext(request_); + + // Body: "list-commands" is natively supported by the command manager. + request_->context()->body_ = "{ \"command\": \"list-commands\"," + " \"service\": [ ] }"; + + // All requests must be finalized before they can be processed. + ASSERT_NO_THROW(request_->finalize()); + + // Setup hook. + CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); + ASSERT_TRUE(ctx); + HooksConfig& hooks_cfg = ctx->getHooksConfig(); + std::string auth_cfg = "{ \"config\": {\n" + "\"type\": \"basic\",\n" + "\"realm\": \"ISC.ORG\",\n" + "\"clients\": [{\n" + " \"user\": \"foo\",\n" + " \"password\": \"bar\"\n" + " }]}}"; + ConstElementPtr auth_json; + ASSERT_NO_THROW(auth_json = Element::fromJSON(auth_cfg)); + hooks_cfg.add(std::string(BASIC_AUTH_LIBRARY), auth_json); + ASSERT_NO_THROW(hooks_cfg.loadLibraries()); + + HttpResponsePtr response; + ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request_)); + ASSERT_TRUE(response); + + // Request should have no service. + EXPECT_EQ("{ \"command\": \"list-commands\" }", + request_->context()->body_); + + // Response must be convertible to HttpResponseJsonPtr. + HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast< + HttpResponseJson>(response); + ASSERT_TRUE(response_json); + + // Response must contain Unauthorized status code. + std::string expected = "HTTP/1.1 401 Unauthorized"; + EXPECT_TRUE(response_json->toString().find(expected) != std::string::npos); + // Reponse should contain WWW-Authenticate header with configured realm. + expected = "WWW-Authenticate: Basic realm=\"ISC.ORG\""; + EXPECT_TRUE(response_json->toString().find(expected) != std::string::npos); +} + +// Test successful server response when the client is authenticated. +TEST_F(CtrlAgentResponseCreatorTest, hookBasicAuth) { + setBasicContext(request_); + + // Body: "list-commands" is natively supported by the command manager. + request_->context()->body_ = "{ \"command\": \"list-commands\" }"; + + // Add basic HTTP authentication header. + const BasicHttpAuth& basic_auth = BasicHttpAuth("foo", "bar"); + const BasicAuthHttpHeaderContext& basic_auth_header = + BasicAuthHttpHeaderContext(basic_auth); + request_->context()->headers_.push_back(basic_auth_header); + + // All requests must be finalized before they can be processed. + ASSERT_NO_THROW(request_->finalize()); + + // Setup hook. + CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); + ASSERT_TRUE(ctx); + HooksConfig& hooks_cfg = ctx->getHooksConfig(); + std::string auth_cfg = "{ \"config\": {\n" + "\"type\": \"basic\",\n" + "\"realm\": \"ISC.ORG\",\n" + "\"clients\": [{\n" + " \"user\": \"foo\",\n" + " \"password\": \"bar\"\n" + " }]}}"; + ConstElementPtr auth_json; + ASSERT_NO_THROW(auth_json = Element::fromJSON(auth_cfg)); + hooks_cfg.add(std::string(BASIC_AUTH_LIBRARY), auth_json); + ASSERT_NO_THROW(hooks_cfg.loadLibraries()); + + 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") != + std::string::npos); + // Response must contain JSON body with "result" of 0. + EXPECT_TRUE(response_json->toString().find("\"result\": 0") != + std::string::npos); + // Response must contain JSON body with "comment": "got". + EXPECT_TRUE(response_json->toString().find("\"comment\": \"got\"") != + std::string::npos); +} + } diff --git a/src/bin/agent/tests/basic_library.cc b/src/bin/agent/tests/callout_library.cc similarity index 100% rename from src/bin/agent/tests/basic_library.cc rename to src/bin/agent/tests/callout_library.cc diff --git a/src/bin/agent/tests/get_config_unittest.cc b/src/bin/agent/tests/get_config_unittest.cc index b028ea8199..b9227bea1a 100644 --- a/src/bin/agent/tests/get_config_unittest.cc +++ b/src/bin/agent/tests/get_config_unittest.cc @@ -105,7 +105,7 @@ pathReplacer(ConstElementPtr ca_cfg) { return; } ElementPtr first_lib = hooks_libs->getNonConst(0); - std::string lib_path(BASIC_CALLOUT_LIBRARY); + std::string lib_path(CALLOUT_LIBRARY); first_lib->set("library", Element::create(lib_path)); } diff --git a/src/bin/agent/tests/test_libraries.h.in b/src/bin/agent/tests/test_libraries.h.in index 4602c4c81f..77cee3522c 100644 --- a/src/bin/agent/tests/test_libraries.h.in +++ b/src/bin/agent/tests/test_libraries.h.in @@ -1,4 +1,4 @@ -// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2017-2020 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 @@ -16,8 +16,11 @@ namespace { // .so file. Note that we access the .so file - libtool creates this as a // like to the real shared library. -// Basic library with context_create and three "standard" callouts. -static const char* BASIC_CALLOUT_LIBRARY = "@abs_builddir@/.libs/libbasic.so"; +// Basic callout library with context_create and three "standard" callouts. +static const char* CALLOUT_LIBRARY = "@abs_builddir@/.libs/libcallout.so"; + +// Basic HTTP authentication as a callout library. +static const char* BASIC_AUTH_LIBRARY = "@abs_builddir@/.libs/libbasicauth.so"; } // anonymous namespace diff --git a/src/bin/netconf/tests/control_socket_unittests.cc b/src/bin/netconf/tests/control_socket_unittests.cc index 7525f74bce..5f754cb284 100644 --- a/src/bin/netconf/tests/control_socket_unittests.cc +++ b/src/bin/netconf/tests/control_socket_unittests.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2018-2019 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2018-2020 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 @@ -430,7 +430,7 @@ protected: /// @param request Pointer to the HTTP request. /// @return Pointer to the generated HTTP response. virtual HttpResponsePtr - createStockHttpResponse(const ConstHttpRequestPtr& request, + createStockHttpResponse(const HttpRequestPtr& request, const HttpStatusCode& status_code) const { // Data is in the request context. HttpVersion http_version(request->context()->http_version_major_, @@ -448,10 +448,10 @@ protected: /// @param request Pointer to the HTTP request. /// @return Pointer to an object representing HTTP response. virtual HttpResponsePtr - createDynamicHttpResponse(const ConstHttpRequestPtr& request) { + createDynamicHttpResponse(HttpRequestPtr request) { // Request must always be JSON. - ConstPostHttpRequestJsonPtr request_json = - boost::dynamic_pointer_cast(request); + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast(request); if (!request_json) { isc_throw(Unexpected, "request is not JSON"); } diff --git a/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc b/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc index 824c5c5446..54d76fe6c4 100644 --- a/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc +++ b/src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc @@ -226,7 +226,7 @@ public: } /// @brief Returns a vector of received requests. - std::vector getReceivedRequests() { + std::vector getReceivedRequests() { return (requests_); } @@ -240,7 +240,7 @@ public: /// /// @return Pointer to the request found, or null pointer if there is /// no such request. - ConstPostHttpRequestJsonPtr + PostHttpRequestJsonPtr findRequest(const std::string& str1, const std::string& str2, const std::string& str3 = "") { for (auto r = requests_.begin(); r < requests_.end(); ++r) { @@ -255,7 +255,7 @@ public: } // Request not found. - return (ConstPostHttpRequestJsonPtr()); + return (PostHttpRequestJsonPtr()); } /// @brief Sets control result to be included in the responses. @@ -333,7 +333,7 @@ private: /// @param request Pointer to the HTTP request. /// @return Pointer to the generated HTTP response. virtual HttpResponsePtr - createStockHttpResponse(const ConstHttpRequestPtr& request, + createStockHttpResponse(const HttpRequestPtr& request, const HttpStatusCode& status_code) const { // The request hasn't been finalized so the request object // doesn't contain any information about the HTTP version number @@ -357,7 +357,7 @@ private: /// @param request Pointer to the HTTP request. /// @return Pointer to the generated HTTP OK response. virtual HttpResponsePtr - createDynamicHttpResponse(const ConstHttpRequestPtr& request) { + createDynamicHttpResponse(HttpRequestPtr request) { // Check authentication. const BasicHttpAuthMap& credentials = getCredentials(); if (!credentials.empty()) { @@ -384,8 +384,8 @@ private: } // Request must always be JSON. - ConstPostHttpRequestJsonPtr request_json = - boost::dynamic_pointer_cast(request); + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast(request); // Remember the request received. requests_.push_back(request_json); @@ -474,7 +474,7 @@ private: } /// @brief Holds received HTTP requests. - std::vector requests_; + std::vector requests_; /// @brief Control result to be returned in the server responses. int control_result_; diff --git a/src/lib/http/Makefile.am b/src/lib/http/Makefile.am index a2acdcb8ca..4f8b77a2d7 100644 --- a/src/lib/http/Makefile.am +++ b/src/lib/http/Makefile.am @@ -48,6 +48,7 @@ libkea_http_la_LDFLAGS = $(AM_LDFLAGS) libkea_http_la_LDFLAGS += -no-undefined -version-info 20:0:0 libkea_http_la_LIBADD = +libkea_http_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la libkea_http_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la libkea_http_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la libkea_http_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la diff --git a/src/lib/http/auth_config.h b/src/lib/http/auth_config.h index a2450b50d8..3f76033f01 100644 --- a/src/lib/http/auth_config.h +++ b/src/lib/http/auth_config.h @@ -67,7 +67,7 @@ public: /// @return Error HTTP response if validation failed, null otherwise. virtual isc::http::HttpResponseJsonPtr checkAuth(const isc::http::HttpResponseCreator& creator, - const isc::http::ConstHttpRequestPtr& request) const = 0; + const isc::http::HttpRequestPtr& request) const = 0; private: diff --git a/src/lib/http/basic_auth_config.cc b/src/lib/http/basic_auth_config.cc index 9dd8577ab8..87106663aa 100644 --- a/src/lib/http/basic_auth_config.cc +++ b/src/lib/http/basic_auth_config.cc @@ -202,7 +202,7 @@ BasicHttpAuthConfig::parse(const ConstElementPtr& config) { HttpResponseJsonPtr BasicHttpAuthConfig::checkAuth(const HttpResponseCreator& creator, - const ConstHttpRequestPtr& request) const { + const HttpRequestPtr& request) const { const BasicHttpAuthMap& credentials = getCredentialMap(); bool authentic = false; if (credentials.empty()) { diff --git a/src/lib/http/basic_auth_config.h b/src/lib/http/basic_auth_config.h index ec88e12ff1..80417d7dd7 100644 --- a/src/lib/http/basic_auth_config.h +++ b/src/lib/http/basic_auth_config.h @@ -121,7 +121,7 @@ public: /// @return Error HTTP response if validation failed, null otherwise. virtual isc::http::HttpResponseJsonPtr checkAuth(const isc::http::HttpResponseCreator& creator, - const isc::http::ConstHttpRequestPtr& request) const; + const isc::http::HttpRequestPtr& request) const; private: diff --git a/src/lib/http/post_request_json.h b/src/lib/http/post_request_json.h index 6d7f82a8af..7a2faba2b3 100644 --- a/src/lib/http/post_request_json.h +++ b/src/lib/http/post_request_json.h @@ -27,8 +27,6 @@ class PostHttpRequestJson; /// @brief Pointer to @ref PostHttpRequestJson. typedef boost::shared_ptr PostHttpRequestJsonPtr; -/// @brief Pointer to const @ref PostHttpRequestJson. -typedef boost::shared_ptr ConstPostHttpRequestJsonPtr; /// @brief Represents HTTP POST request with JSON body. /// diff --git a/src/lib/http/request.h b/src/lib/http/request.h index 1b26b1c48f..b5ce5ac55c 100644 --- a/src/lib/http/request.h +++ b/src/lib/http/request.h @@ -7,6 +7,7 @@ #ifndef HTTP_REQUEST_H #define HTTP_REQUEST_H +#include #include #include #include @@ -28,9 +29,6 @@ class HttpRequest; /// @brief Pointer to the @ref HttpRequest object. typedef boost::shared_ptr HttpRequestPtr; -/// @brief Pointer to the const @ref HttpRequest object. -typedef boost::shared_ptr ConstHttpRequestPtr; - /// @brief Represents HTTP request message. /// /// This derivation of the @c HttpMessage class is specialized to represent @@ -46,7 +44,9 @@ typedef boost::shared_ptr ConstHttpRequestPtr; /// which derives from @c PostHttpRequest requires that the POST message /// includes body holding a JSON structure and provides methods to parse the /// JSON body. -class HttpRequest : public HttpMessage { +/// +/// Callouts are associated to the request. +class HttpRequest : public HttpMessage, public hooks::CalloutHandleAssociate { public: /// @brief HTTP methods. diff --git a/src/lib/http/response_creator.cc b/src/lib/http/response_creator.cc index 5a310e22b5..a060f6ad0b 100644 --- a/src/lib/http/response_creator.cc +++ b/src/lib/http/response_creator.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2016-2020 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 @@ -12,7 +12,7 @@ namespace isc { namespace http { HttpResponsePtr -HttpResponseCreator::createHttpResponse(const ConstHttpRequestPtr& request) { +HttpResponseCreator::createHttpResponse(HttpRequestPtr request) { // This should never happen. This method must only be called with a // non null request, so we consider it unlikely internal server error. if (!request) { diff --git a/src/lib/http/response_creator.h b/src/lib/http/response_creator.h index dbae326ba9..ac212fe5de 100644 --- a/src/lib/http/response_creator.h +++ b/src/lib/http/response_creator.h @@ -1,4 +1,4 @@ -// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2016-2020 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 @@ -74,7 +74,7 @@ public: /// @return Pointer to the object encapsulating generated HTTP response. /// @throw HttpResponseError if request is a NULL pointer. virtual HttpResponsePtr - createHttpResponse(const ConstHttpRequestPtr& request) final; + createHttpResponse(HttpRequestPtr request) final; /// @brief Create a new request. /// @@ -93,7 +93,7 @@ public: /// @param status_code Status code of the response. /// @return Pointer to an object representing HTTP response. virtual HttpResponsePtr - createStockHttpResponse(const ConstHttpRequestPtr& request, + createStockHttpResponse(const HttpRequestPtr& request, const HttpStatusCode& status_code) const = 0; protected: @@ -103,7 +103,7 @@ protected: /// @param request Pointer to an object representing HTTP request. /// @return Pointer to an object representing HTTP response. virtual HttpResponsePtr - createDynamicHttpResponse(const ConstHttpRequestPtr& request) = 0; + createDynamicHttpResponse(HttpRequestPtr request) = 0; }; diff --git a/src/lib/http/tests/Makefile.am b/src/lib/http/tests/Makefile.am index 6dd63a1d5e..fb38443148 100644 --- a/src/lib/http/tests/Makefile.am +++ b/src/lib/http/tests/Makefile.am @@ -44,6 +44,7 @@ libhttp_unittests_CXXFLAGS = $(AM_CXXFLAGS) libhttp_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) libhttp_unittests_LDADD = $(top_builddir)/src/lib/http/libkea-http.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la libhttp_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la libhttp_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la libhttp_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la diff --git a/src/lib/http/tests/connection_pool_unittests.cc b/src/lib/http/tests/connection_pool_unittests.cc index 42fda5ff22..13c4bd52bb 100644 --- a/src/lib/http/tests/connection_pool_unittests.cc +++ b/src/lib/http/tests/connection_pool_unittests.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2017-2020 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 @@ -55,7 +55,7 @@ private: /// @param request Pointer to the HTTP request. /// @return Pointer to the generated HTTP response. virtual HttpResponsePtr - createStockHttpResponse(const ConstHttpRequestPtr& request, + createStockHttpResponse(const HttpRequestPtr& request, const HttpStatusCode& status_code) const { // The request hasn't been finalized so the request object // doesn't contain any information about the HTTP version number @@ -73,7 +73,7 @@ private: /// @param request Pointer to the HTTP request. /// @return Pointer to the generated HTTP OK response with no content. virtual HttpResponsePtr - createDynamicHttpResponse(const ConstHttpRequestPtr& request) { + createDynamicHttpResponse(HttpRequestPtr request) { // The simplest thing is to create a response with no content. // We don't need content to test our class. ResponsePtr response(new Response(request->getHttpVersion(), diff --git a/src/lib/http/tests/response_creator_unittests.cc b/src/lib/http/tests/response_creator_unittests.cc index 1b2f9fdfb8..3e0f1078c3 100644 --- a/src/lib/http/tests/response_creator_unittests.cc +++ b/src/lib/http/tests/response_creator_unittests.cc @@ -49,7 +49,7 @@ private: /// @param request Pointer to the HTTP request. /// @return Pointer to the generated HTTP response. virtual HttpResponsePtr - createStockHttpResponse(const ConstHttpRequestPtr& request, + createStockHttpResponse(const HttpRequestPtr& request, const HttpStatusCode& status_code) const { // The request hasn't been finalized so the request object // doesn't contain any information about the HTTP version number @@ -68,7 +68,7 @@ private: /// @param request Pointer to the HTTP request. /// @return Pointer to the generated HTTP OK response with no content. virtual HttpResponsePtr - createDynamicHttpResponse(const ConstHttpRequestPtr& request) { + createDynamicHttpResponse(HttpRequestPtr request) { // The simplest thing is to create a response with no content. // We don't need content to test our class. ResponsePtr response(new Response(request->getHttpVersion(), diff --git a/src/lib/http/tests/server_client_unittests.cc b/src/lib/http/tests/server_client_unittests.cc index d55b3c2aa0..ab123191d1 100644 --- a/src/lib/http/tests/server_client_unittests.cc +++ b/src/lib/http/tests/server_client_unittests.cc @@ -93,7 +93,7 @@ private: /// @param request Pointer to the HTTP request. /// @return Pointer to the generated HTTP response. virtual HttpResponsePtr - createStockHttpResponse(const ConstHttpRequestPtr& request, + createStockHttpResponse(const HttpRequestPtr& request, const HttpStatusCode& status_code) const { // The request hasn't been finalized so the request object // doesn't contain any information about the HTTP version number @@ -122,10 +122,10 @@ private: /// @param request Pointer to the HTTP request. /// @return Pointer to the generated HTTP OK response with no content. virtual HttpResponsePtr - createDynamicHttpResponse(const ConstHttpRequestPtr& request) { + createDynamicHttpResponse(HttpRequestPtr request) { // Request must always be JSON. - ConstPostHttpRequestJsonPtr request_json = - boost::dynamic_pointer_cast(request); + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast(request); ConstElementPtr body; if (request_json) { body = request_json->getBodyAsJson();