1 // Copyright (C) 2017-2020 Internet Systems Consortium, Inc. ("ISC")
2 //
3 // This Source Code Form is subject to the terms of the Mozilla Public
4 // License, v. 2.0. If a copy of the MPL was not distributed with this
5 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 
7 #include <config.h>
8 #include <agent/ca_controller.h>
9 #include <agent/ca_process.h>
10 #include <agent/ca_command_mgr.h>
11 #include <agent/ca_response_creator.h>
12 #include <cc/command_interpreter.h>
13 #include <cryptolink/crypto_rng.h>
14 #include <hooks/hooks_manager.h>
15 #include <http/basic_auth_config.h>
16 #include <http/post_request.h>
17 #include <http/post_request_json.h>
18 #include <http/response_json.h>
19 #include <process/testutils/d_test_stubs.h>
20 #include <agent/tests/test_basic_auth_libraries.h>
21 #include <gtest/gtest.h>
22 #include <boost/pointer_cast.hpp>
23 #include <functional>
24 
25 using namespace isc;
26 using namespace isc::agent;
27 using namespace isc::config;
28 using namespace isc::data;
29 using namespace isc::hooks;
30 using namespace isc::http;
31 using namespace isc::process;
32 namespace ph = std::placeholders;
33 
34 namespace {
35 
36 /// @brief Test fixture class for @ref CtrlAgentResponseCreator.
37 class CtrlAgentResponseCreatorTest : public DControllerTest {
38 public:
39 
40     /// @brief Constructor.
41     ///
42     /// Creates instance of the response creator and uses this instance to
43     /// create "empty" request. It also removes registered commands from the
44     /// command manager.
CtrlAgentResponseCreatorTest()45     CtrlAgentResponseCreatorTest()
46         : DControllerTest(CtrlAgentController::instance),
47           response_creator_(),
48           request_(response_creator_.createNewHttpRequest()) {
49         // Deregisters commands.
50         CtrlAgentCommandMgr::instance().deregisterAll();
51         CtrlAgentCommandMgr::instance().
52             registerCommand("foo", std::bind(&CtrlAgentResponseCreatorTest::
53                                              fooCommandHandler,
54                                              this, ph::_1, ph::_2));
55 
56         // Make sure that the request has been initialized properly.
57         if (!request_) {
58             ADD_FAILURE() << "CtrlAgentResponseCreator::createNewHttpRequest"
59                 " returns NULL!";
60         }
61         // Initialize process and cfgmgr.
62         try {
63             initProcess();
64             static_cast<void>(getCtrlAgentCfgContext());
65         } catch (const std::exception& ex) {
66             ADD_FAILURE() << "Initialization failed: " << ex.what();
67         }
68     }
69 
70     /// @brief Destructor.
71     ///
72     /// Removes registered commands from the command manager.
~CtrlAgentResponseCreatorTest()73     virtual ~CtrlAgentResponseCreatorTest() {
74         CtrlAgentCommandMgr::instance().deregisterAll();
75         HooksManager::prepareUnloadLibraries();
76         static_cast<void>(HooksManager::unloadLibraries());
77     }
78 
79     /// @brief Fills request context with required data to create new request.
80     ///
81     /// @param request Request which context should be configured.
setBasicContext(const HttpRequestPtr & request)82     void setBasicContext(const HttpRequestPtr& request) {
83         request->context()->method_ = "POST";
84         request->context()->http_version_major_ = 1;
85         request->context()->http_version_minor_ = 1;
86         request->context()->uri_ = "/foo";
87 
88         // Content-Type
89         HttpHeaderContext content_type;
90         content_type.name_ = "Content-Type";
91         content_type.value_ = "application/json";
92         request->context()->headers_.push_back(content_type);
93 
94         // Content-Length
95         HttpHeaderContext content_length;
96         content_length.name_ = "Content-Length";
97         content_length.value_ = "0";
98         request->context()->headers_.push_back(content_length);
99     }
100 
101     /// @brief Test creation of stock response.
102     ///
103     /// @param status_code Status code to be included in the response.
104     /// @param must_contain Text that must be present in the textual
105     /// representation of the generated response.
testStockResponse(const HttpStatusCode & status_code,const std::string & must_contain)106     void testStockResponse(const HttpStatusCode& status_code,
107                            const std::string& must_contain) {
108         HttpResponsePtr response;
109         ASSERT_NO_THROW(
110             response = response_creator_.createStockHttpResponse(request_,
111                                                                  status_code)
112         );
113         ASSERT_TRUE(response);
114         HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
115             HttpResponseJson>(response);
116         ASSERT_TRUE(response_json);
117         // Make sure the response contains the string specified as argument.
118         EXPECT_TRUE(response_json->toString().find(must_contain) != std::string::npos);
119 
120     }
121 
122     /// @brief Handler for the 'foo' test command.
123     ///
124     /// @param command_name Command name, i.e. 'foo'.
125     /// @param command_arguments Command arguments (empty).
126     ///
127     /// @return Returns response with a single string "bar".
fooCommandHandler(const std::string &,const ConstElementPtr &)128     ConstElementPtr fooCommandHandler(const std::string& /*command_name*/,
129                                       const ConstElementPtr& /*command_arguments*/) {
130         ElementPtr arguments = Element::createList();
131         arguments->add(Element::create("bar"));
132         return (createAnswer(CONTROL_RESULT_SUCCESS, arguments));
133     }
134 
135     /// @brief Returns a pointer to the configuration context.
getCtrlAgentCfgContext()136     CtrlAgentCfgContextPtr getCtrlAgentCfgContext() {
137         CtrlAgentProcessPtr process =
138             boost::dynamic_pointer_cast<CtrlAgentProcess>(getProcess());
139         if (!process) {
140             isc_throw(Unexpected, "no process");
141         }
142         CtrlAgentCfgMgrPtr cfgmgr = process->getCtrlAgentCfgMgr();
143         if (!cfgmgr) {
144             isc_throw(Unexpected, "no cfgmgr");
145         }
146         CtrlAgentCfgContextPtr ctx = cfgmgr->getCtrlAgentCfgContext();
147         if (!ctx) {
148             isc_throw(Unexpected, "no context");
149         }
150         return (ctx);
151     }
152 
153     /// @brief Instance of the response creator.
154     CtrlAgentResponseCreator response_creator_;
155 
156     /// @brief Instance of the "empty" request.
157     ///
158     /// The context belonging to this request may be modified by the unit
159     /// tests to verify various scenarios of response creation.
160     HttpRequestPtr request_;
161 };
162 
163 // This test verifies that the created "empty" request has valid type.
TEST_F(CtrlAgentResponseCreatorTest,createNewHttpRequest)164 TEST_F(CtrlAgentResponseCreatorTest, createNewHttpRequest) {
165     // The request must be of PostHttpRequestJson type.
166     PostHttpRequestJsonPtr request_json = boost::dynamic_pointer_cast<
167         PostHttpRequestJson>(request_);
168     ASSERT_TRUE(request_json);
169 }
170 
171 // Test that HTTP version of stock response is set to 1.0 if the request
172 // context doesn't specify any version.
TEST_F(CtrlAgentResponseCreatorTest,createStockHttpResponseNoVersion)173 TEST_F(CtrlAgentResponseCreatorTest, createStockHttpResponseNoVersion) {
174     testStockResponse(HttpStatusCode::BAD_REQUEST, "HTTP/1.0 400 Bad Request");
175 }
176 
177 // Test that HTTP version of stock response is set to 1.0 if the request
178 // version is higher than 1.1.
TEST_F(CtrlAgentResponseCreatorTest,createStockHttpResponseHighVersion)179 TEST_F(CtrlAgentResponseCreatorTest, createStockHttpResponseHighVersion) {
180     request_->context()->http_version_major_ = 2;
181     testStockResponse(HttpStatusCode::REQUEST_TIMEOUT,
182                       "HTTP/1.0 408 Request Timeout");
183 }
184 
185 // Test that the server responds with version 1.1 if request version is 1.1.
TEST_F(CtrlAgentResponseCreatorTest,createStockHttpResponseCorrectVersion)186 TEST_F(CtrlAgentResponseCreatorTest, createStockHttpResponseCorrectVersion) {
187     request_->context()->http_version_major_ = 1;
188     request_->context()->http_version_minor_ = 1;
189     testStockResponse(HttpStatusCode::NO_CONTENT, "HTTP/1.1 204 No Content");
190 }
191 
192 // Test successful server response when the client specifies valid command.
TEST_F(CtrlAgentResponseCreatorTest,createDynamicHttpResponse)193 TEST_F(CtrlAgentResponseCreatorTest, createDynamicHttpResponse) {
194     setBasicContext(request_);
195 
196     // Body: "foo" command has been registered in the test fixture constructor.
197     request_->context()->body_ = "{ \"command\": \"foo\" }";
198 
199     // All requests must be finalized before they can be processed.
200     ASSERT_NO_THROW(request_->finalize());
201 
202     // Create response from the request.
203     HttpResponsePtr response;
204     ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request_));
205     ASSERT_TRUE(response);
206 
207     // Response must be convertible to HttpResponseJsonPtr.
208     HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
209         HttpResponseJson>(response);
210     ASSERT_TRUE(response_json);
211 
212     // Response must be successful.
213     EXPECT_TRUE(response_json->toString().find("HTTP/1.1 200 OK") !=
214                 std::string::npos);
215     // Response must contain JSON body with "result" of 0.
216     EXPECT_TRUE(response_json->toString().find("\"result\": 0") !=
217                 std::string::npos);
218 }
219 
220 // This test verifies that Internal Server Error is returned when invalid C++
221 // request type is used. This is considered an error in the server logic.
TEST_F(CtrlAgentResponseCreatorTest,createDynamicHttpResponseInvalidType)222 TEST_F(CtrlAgentResponseCreatorTest, createDynamicHttpResponseInvalidType) {
223     PostHttpRequestPtr request(new PostHttpRequest());
224     setBasicContext(request);
225 
226     // Body: "list-commands" is natively supported by the command manager.
227     request->context()->body_ = "{ \"command\": \"list-commands\" }";
228 
229     // All requests must be finalized before they can be processed.
230     ASSERT_NO_THROW(request->finalize());
231 
232     HttpResponsePtr response;
233     ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request));
234     ASSERT_TRUE(response);
235 
236     // Response must be convertible to HttpResponseJsonPtr.
237     HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
238         HttpResponseJson>(response);
239     ASSERT_TRUE(response_json);
240 
241     // Response must contain Internal Server Error status code.
242     EXPECT_TRUE(response_json->toString().find("HTTP/1.1 500 Internal Server Error") !=
243                 std::string::npos);
244 }
245 
246 // This test verifies that Unauthorized is returned when authentication is
247 // required but not provided by request.
TEST_F(CtrlAgentResponseCreatorTest,noAuth)248 TEST_F(CtrlAgentResponseCreatorTest, noAuth) {
249     setBasicContext(request_);
250 
251     // Body: "list-commands" is natively supported by the command manager.
252     request_->context()->body_ = "{ \"command\": \"list-commands\" }";
253 
254     // All requests must be finalized before they can be processed.
255     ASSERT_NO_THROW(request_->finalize());
256 
257     // Require authentication.
258     CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
259     ASSERT_TRUE(ctx);
260     BasicHttpAuthConfigPtr auth(new BasicHttpAuthConfig());
261     ASSERT_NO_THROW(ctx->setAuthConfig(auth));
262     auth->setRealm("ISC.ORG");
263     auth->add("foo", "bar");
264 
265     HttpResponsePtr response;
266     ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request_));
267     ASSERT_TRUE(response);
268 
269     // Response must be convertible to HttpResponseJsonPtr.
270     HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
271         HttpResponseJson>(response);
272     ASSERT_TRUE(response_json);
273 
274     // Response must contain Unauthorized status code.
275     std::string expected = "HTTP/1.1 401 Unauthorized";
276     EXPECT_TRUE(response_json->toString().find(expected) != std::string::npos);
277     // Reponse should contain WWW-Authenticate header with configured realm.
278     expected = "WWW-Authenticate: Basic realm=\"ISC.ORG\"";
279     EXPECT_TRUE(response_json->toString().find(expected) != std::string::npos);
280 }
281 
282 // Test successful server response when the client is authenticated.
TEST_F(CtrlAgentResponseCreatorTest,basicAuth)283 TEST_F(CtrlAgentResponseCreatorTest, basicAuth) {
284     setBasicContext(request_);
285 
286     // Body: "list-commands" is natively supported by the command manager.
287     request_->context()->body_ = "{ \"command\": \"list-commands\" }";
288 
289     // Add basic HTTP authentication header.
290     const BasicHttpAuth& basic_auth = BasicHttpAuth("foo", "bar");
291     const BasicAuthHttpHeaderContext& basic_auth_header =
292         BasicAuthHttpHeaderContext(basic_auth);
293     request_->context()->headers_.push_back(basic_auth_header);
294 
295     // All requests must be finalized before they can be processed.
296     ASSERT_NO_THROW(request_->finalize());
297 
298     // Require authentication.
299     CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
300     ASSERT_TRUE(ctx);
301     BasicHttpAuthConfigPtr auth(new BasicHttpAuthConfig());
302     ASSERT_NO_THROW(ctx->setAuthConfig(auth));
303     // In fact the realm is used only on errors... set it anyway.
304     auth->setRealm("ISC.ORG");
305     auth->add("foo", "bar");
306 
307     HttpResponsePtr response;
308     ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request_));
309     ASSERT_TRUE(response);
310 
311     // Response must be convertible to HttpResponseJsonPtr.
312     HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
313         HttpResponseJson>(response);
314     ASSERT_TRUE(response_json);
315 
316     // Response must be successful.
317     EXPECT_TRUE(response_json->toString().find("HTTP/1.1 200 OK") !=
318                 std::string::npos);
319     // Response must contain JSON body with "result" of 0.
320     EXPECT_TRUE(response_json->toString().find("\"result\": 0") !=
321                 std::string::npos);
322 }
323 
324 // This test verifies that Unauthorized is returned when authentication is
325 // required but not provided by request using the hook.
TEST_F(CtrlAgentResponseCreatorTest,hookNoAuth)326 TEST_F(CtrlAgentResponseCreatorTest, hookNoAuth) {
327     setBasicContext(request_);
328 
329     // Body: "list-commands" is natively supported by the command manager.
330     // We add a random value in the extra entry: see next unit test
331     // for an explanation about how it is used.
332     auto r32 = isc::cryptolink::random(4);
333     ASSERT_EQ(4, r32.size());
334     int extra = r32[0];
335     extra = (extra << 8) | r32[1];
336     extra = (extra << 8) | r32[2];
337     extra = (extra << 8) | r32[3];
338     request_->context()->body_ = "{ \"command\": \"list-commands\", ";
339     request_->context()->body_ += "\"extra\": " + std::to_string(extra) + " }";
340 
341     // All requests must be finalized before they can be processed.
342     ASSERT_NO_THROW(request_->finalize());
343 
344     // Setup hook.
345     CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
346     ASSERT_TRUE(ctx);
347     HooksConfig& hooks_cfg = ctx->getHooksConfig();
348     std::string auth_cfg = "{ \"config\": {\n"
349         "\"type\": \"basic\",\n"
350         "\"realm\": \"ISC.ORG\",\n"
351         "\"clients\": [{\n"
352         " \"user\": \"foo\",\n"
353         " \"password\": \"bar\"\n"
354         " }]}}";
355     ConstElementPtr auth_json;
356     ASSERT_NO_THROW(auth_json = Element::fromJSON(auth_cfg));
357     hooks_cfg.add(std::string(BASIC_AUTH_LIBRARY), auth_json);
358     ASSERT_NO_THROW(hooks_cfg.loadLibraries());
359 
360     HttpResponsePtr response;
361     ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request_));
362     ASSERT_TRUE(response);
363 
364     // Request should have no extra.
365     EXPECT_EQ("{ \"command\": \"list-commands\" }",
366               request_->context()->body_);
367 
368     // Response must be convertible to HttpResponseJsonPtr.
369     HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
370         HttpResponseJson>(response);
371     ASSERT_TRUE(response_json);
372 
373     // Response must contain Unauthorized status code.
374     std::string expected = "HTTP/1.1 401 Unauthorized";
375     EXPECT_TRUE(response_json->toString().find(expected) != std::string::npos);
376     // Reponse should contain WWW-Authenticate header with configured realm.
377     expected = "WWW-Authenticate: Basic realm=\"ISC.ORG\"";
378     EXPECT_TRUE(response_json->toString().find(expected) != std::string::npos);
379 }
380 
381 // Test successful server response when the client is authenticated.
TEST_F(CtrlAgentResponseCreatorTest,hookBasicAuth)382 TEST_F(CtrlAgentResponseCreatorTest, hookBasicAuth) {
383     setBasicContext(request_);
384 
385     // Body: "list-commands" is natively supported by the command manager.
386     // We add a random value in the extra entry:
387     //  - this proves that the auth callout can get the request argument
388     //  - this proves that the auth callout can modify the request argument
389     //    before the request is executed (the extra entry if still present
390     //    would make the command to be rejected as malformed)
391     //  - this proves that a value can be communicate between the auth
392     //    and response callout points
393     //  - this proves that the response callout can get the response argument
394     //  - this proves that the response callout can modify the response
395     //    argument
396     auto r32 = isc::cryptolink::random(4);
397     ASSERT_EQ(4, r32.size());
398     int extra = r32[0];
399     extra = (extra << 8) | r32[1];
400     extra = (extra << 8) | r32[2];
401     extra = (extra << 8) | r32[3];
402     if (extra == 0) {
403         extra = 1;
404     }
405     std::string extra_str = std::to_string(extra);
406     request_->context()->body_ = "{ \"command\": \"list-commands\", ";
407     request_->context()->body_ += "\"extra\": " + extra_str + " }";
408 
409     // Add basic HTTP authentication header.
410     const BasicHttpAuth& basic_auth = BasicHttpAuth("foo", "bar");
411     const BasicAuthHttpHeaderContext& basic_auth_header =
412         BasicAuthHttpHeaderContext(basic_auth);
413     request_->context()->headers_.push_back(basic_auth_header);
414 
415     // All requests must be finalized before they can be processed.
416     ASSERT_NO_THROW(request_->finalize());
417 
418     // Setup hook.
419     CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext();
420     ASSERT_TRUE(ctx);
421     HooksConfig& hooks_cfg = ctx->getHooksConfig();
422     std::string auth_cfg = "{ \"config\": {\n"
423         "\"type\": \"basic\",\n"
424         "\"realm\": \"ISC.ORG\",\n"
425         "\"clients\": [{\n"
426         " \"user\": \"foo\",\n"
427         " \"password\": \"bar\"\n"
428         " }]}}";
429     ConstElementPtr auth_json;
430     ASSERT_NO_THROW(auth_json = Element::fromJSON(auth_cfg));
431     hooks_cfg.add(std::string(BASIC_AUTH_LIBRARY), auth_json);
432     ASSERT_NO_THROW(hooks_cfg.loadLibraries());
433 
434     HttpResponsePtr response;
435     ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request_));
436     ASSERT_TRUE(response);
437 
438     // Request should have no extra.
439     EXPECT_EQ("{ \"command\": \"list-commands\" }",
440               request_->context()->body_);
441 
442     // Response must be convertible to HttpResponseJsonPtr.
443     HttpResponseJsonPtr response_json = boost::dynamic_pointer_cast<
444         HttpResponseJson>(response);
445     ASSERT_TRUE(response_json);
446 
447     // Response must be successful.
448     EXPECT_TRUE(response_json->toString().find("HTTP/1.1 200 OK") !=
449                 std::string::npos);
450     // Response must contain JSON body with "result" of 0.
451     EXPECT_TRUE(response_json->toString().find("\"result\": 0") !=
452                 std::string::npos);
453     // Response must contain JSON body with "comment": "got".
454     std::string expected = "\"got\": " + extra_str + ", ";
455     EXPECT_TRUE(response_json->toString().find(expected) !=
456                 std::string::npos);
457 }
458 
459 }
460