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