1 /**
2  *    Copyright (C) 2019-present MongoDB, Inc.
3  *
4  *    This program is free software: you can redistribute it and/or modify
5  *    it under the terms of the Server Side Public License, version 1,
6  *    as published by MongoDB, Inc.
7  *
8  *    This program is distributed in the hope that it will be useful,
9  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
10  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11  *    Server Side Public License for more details.
12  *
13  *    You should have received a copy of the Server Side Public License
14  *    along with this program. If not, see
15  *    <http://www.mongodb.com/licensing/server-side-public-license>.
16  *
17  *    As a special exception, the copyright holders give permission to link the
18  *    code of portions of this program with the OpenSSL library under certain
19  *    conditions as described in each individual source file and distribute
20  *    linked combinations including the program with the OpenSSL library. You
21  *    must comply with the Server Side Public License in all respects for
22  *    all of the code used other than as permitted herein. If you modify file(s)
23  *    with this exception, you may extend this exception to your version of the
24  *    file(s), but you are not obligated to do so. If you do not wish to do so,
25  *    delete this exception statement from your version. If you delete this
26  *    exception statement from all source files in the program, then also delete
27  *    it in the license file.
28  */
29 
30 #include "mongo/platform/basic.h"
31 
32 #include "mongo/db/repl/split_horizon.h"
33 
34 #include <algorithm>
35 #include <boost/optional.hpp>
36 #include <iterator>
37 
38 #include "mongo/stdx/utility.h"
39 #include "mongo/unittest/unittest.h"
40 
41 using namespace std::literals::string_literals;
42 
43 namespace mongo {
44 namespace repl {
45 namespace {
46 static const std::string defaultHost = "default.dns.name.example.com";
47 static const std::string defaultPort = "4242";
48 static const std::string defaultHostAndPort = defaultHost + ":" + defaultPort;
49 
50 static const std::string matchingHost = "matching.dns.name.example.com";
51 static const std::string matchingPort = "4243";
52 static const std::string matchingHostAndPort = matchingHost + ":" + matchingPort;
53 
54 
55 static const std::string nonmatchingHost = "nonmatching.dns.name.example.com";
56 static const std::string nonmatchingPort = "4244";
57 static const std::string nonmatchingHostAndPort = nonmatchingHost + ":" + nonmatchingPort;
58 
59 static const std::string altPort = ":666";
60 
61 using MappingType = std::map<std::string, std::string>;
62 
populateForwardMap(const MappingType & mapping)63 SplitHorizon::ForwardMapping populateForwardMap(const MappingType& mapping) {
64     SplitHorizon::ForwardMapping forwardMapping;
65     forwardMapping.emplace(SplitHorizon::kDefaultHorizon, HostAndPort(defaultHostAndPort));
66 
67     using ForwardMappingValueType = decltype(forwardMapping)::value_type;
68     using ElementType = MappingType::value_type;
69     auto createForwardMapping = [](const ElementType& element) {
70         return ForwardMappingValueType{element.first, HostAndPort(element.second)};
71     };
72     std::transform(begin(mapping),
73                    end(mapping),
74                    inserter(forwardMapping, end(forwardMapping)),
75                    createForwardMapping);
76 
77     return forwardMapping;
78 }
79 
TEST(SplitHorizonTesting,determineHorizon)80 TEST(SplitHorizonTesting, determineHorizon) {
81     struct Input {
82         SplitHorizon::ForwardMapping forwardMapping;  // Will get "__default" added to it.
83         SplitHorizon::Parameters horizonParameters;
84 
85         Input(const MappingType& mapping, boost::optional<std::string> sniName)
86             : forwardMapping(populateForwardMap(mapping)), horizonParameters(std::move(sniName)) {}
87     };
88 
89     struct {
90         Input input;
91 
92         std::string expected;
93     } tests[] = {
94         // No parameters and no horizon views configured.
95         {{{}, boost::none}, "__default"},
96         {{{}, defaultHost}, "__default"},
97 
98         // No SNI -> no match
99         {{{{"unusedHorizon", "badmatch:00001"}}, boost::none}, "__default"},
100 
101         // Unmatching SNI -> no match
102         {{{{"unusedHorizon", "badmatch:00001"}}, nonmatchingHost}, "__default"},
103 
104         // Matching SNI -> match
105         {{{{"targetHorizon", matchingHostAndPort}}, matchingHost}, "targetHorizon"},
106     };
107 
108     for (const auto& test : tests) {
109         const auto& expected = test.expected;
110         const auto& input = test.input;
111 
112         const std::string witness =
113             SplitHorizon(input.forwardMapping).determineHorizon(input.horizonParameters).toString();
114         ASSERT_EQUALS(witness, expected);
115     }
116 
117     const Input failingCtorCases[] = {
118         // Matching SNI, different port, collision -> fails
119         {{{"targetHorizon", matchingHost + altPort}, {"badHorizon", matchingHostAndPort}},
120          matchingHost},
121 
122         // Default horizon ambiguous case is a failure
123         {{{"targetHorizon", defaultHost + altPort}, {"badHorizon", nonmatchingHostAndPort}},
124          defaultHost},
125     };
126 
127     for (const auto& input : failingCtorCases) {
128         ASSERT_THROWS(SplitHorizon(input.forwardMapping), ExceptionFor<ErrorCodes::BadValue>);
129     }
130 }
131 
TEST(SplitHorizonTesting,basicConstruction)132 TEST(SplitHorizonTesting, basicConstruction) {
133     struct Input {
134         SplitHorizon::ForwardMapping forwardMapping;  // Will get "__default" added to it.
135 
136         Input(const MappingType& mapping) : forwardMapping(populateForwardMap(mapping)) {}
137     };
138 
139     const struct {
140         Input input;
141         ErrorCodes::Error expectedErrorCode;
142         std::vector<std::string> expectedErrorMessageFragments;
143         std::vector<std::string> absentErrorMessageFragments;
144     } tests[] = {
145         // Empty case (the `Input` type constructs the expected "__default" member.)
146         {{{}}, ErrorCodes::OK, {}, {}},
147 
148         // A single horizon case, with no conflicts.
149         {{{{"extraHorizon", "example.com:42"}}}, ErrorCodes::OK, {}, {}},
150 
151         // Two horizons with no conflicts
152         {{{{"extraHorizon", "example.com:42"}, {"extraHorizon2", "extra.example.com:42"}}},
153          ErrorCodes::OK,
154          {},
155          {}},
156 
157         // Three horizons with no conflicts
158         {{{{"extraHorizon", "example.com:42"},
159            {"extraHorizon2", "extra.example.com:42"},
160            {"extraHorizon3", "extra3.example.com" + altPort}}},
161          ErrorCodes::OK,
162          {},
163          {}},
164 
165         // Two horizons, with the same host and port
166         {{{{"horizon1", "same.example.com:42"}, {"horizon2", "same.example.com:42"}}},
167          ErrorCodes::BadValue,
168          {"Duplicate horizon member found", "same.example.com"},
169          {}},
170 
171         // Two horizons, with the same host and different port
172         {{{{"horizon1", "same.example.com:42"}, {"horizon2", "same.example.com:43"}}},
173          ErrorCodes::BadValue,
174          {"Duplicate horizon member found", "same.example.com"},
175          {}},
176 
177         // Three horizons, with two of them having the same host and port (checking that
178         // the distinct horizon isn't reported in the error message.
179         {{{{"horizon1", "same.example.com:42"},
180            {"horizon2", "different.example.com:42"},
181            {"horizon3", "same.example.com:42"}}},
182          ErrorCodes::BadValue,
183          {"Duplicate horizon member found", "same.example.com"},
184          {"different.example.com"}},
185     };
186 
187     for (const auto& test : tests) {
188         const auto& input = test.input;
189         const auto& expectedErrorCode = test.expectedErrorCode;
190         const auto horizonOpt = [&]() -> boost::optional<SplitHorizon> {
191             try {
192                 return SplitHorizon(input.forwardMapping);
193             } catch (const DBException& ex) {
194                 ASSERT_NOT_EQUALS(expectedErrorCode, ErrorCodes::OK);
195                 ASSERT_EQUALS(ex.toStatus().code(), expectedErrorCode);
196                 for (const auto& fragment : test.expectedErrorMessageFragments) {
197                     ASSERT_NOT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos);
198                 }
199                 for (const auto& fragment : test.absentErrorMessageFragments) {
200                     ASSERT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos);
201                 }
202                 return boost::none;
203             }
204         }();
205 
206         if (!horizonOpt)
207             continue;
208         ASSERT_EQUALS(expectedErrorCode, ErrorCodes::OK);
209 
210         const auto& horizon = *horizonOpt;
211 
212         for (const auto& element : input.forwardMapping) {
213             {
214                 const auto found = horizon.getForwardMappings().find(element.first);
215                 ASSERT_TRUE(found != end(horizon.getForwardMappings()));
216                 ASSERT_EQUALS(HostAndPort(element.second).toString(), found->second.toString());
217             }
218 
219             {
220                 const auto found =
221                     horizon.getReverseHostMappings().find(HostAndPort(element.second).host());
222                 ASSERT_TRUE(found != end(horizon.getReverseHostMappings()));
223                 ASSERT_EQUALS(element.first, found->second);
224             }
225         }
226         ASSERT_EQUALS(input.forwardMapping.size(), horizon.getForwardMappings().size());
227         ASSERT_EQUALS(input.forwardMapping.size(), horizon.getReverseHostMappings().size());
228     }
229 }
230 
TEST(SplitHorizonTesting,BSONConstruction)231 TEST(SplitHorizonTesting, BSONConstruction) {
232     // The none-case can be tested outside ot the table, to help keep the table ctors
233     // easier.
234     {
235         const SplitHorizon horizon(HostAndPort(matchingHostAndPort), boost::none);
236 
237         {
238             const auto forwardFound = horizon.getForwardMappings().find("__default");
239             ASSERT_TRUE(forwardFound != end(horizon.getForwardMappings()));
240             ASSERT_EQUALS(forwardFound->second, HostAndPort(matchingHostAndPort));
241             ASSERT_EQUALS(horizon.getForwardMappings().size(), std::size_t{1});
242         }
243 
244         {
245             const auto reverseFound = horizon.getReverseHostMappings().find(matchingHost);
246             ASSERT_TRUE(reverseFound != end(horizon.getReverseHostMappings()));
247             ASSERT_EQUALS(reverseFound->second, "__default");
248 
249             ASSERT_EQUALS(horizon.getReverseHostMappings().size(), std::size_t{1});
250         }
251     }
252 
253     const struct {
254         BSONObj bsonContents;
255         std::string host;
256         std::vector<std::pair<std::string, std::string>> expectedMapping;  // bidirectional
257         ErrorCodes::Error expectedErrorCode;
258         std::vector<std::string> expectedErrorMessageFragments;
259         std::vector<std::string> absentErrorMessageFragments;
260     } tests[] = {
261         // Empty bson object
262         {BSONObj(),
263          defaultHostAndPort,
264          {},
265          ErrorCodes::BadValue,
266          {"horizons field cannot be empty, if present"},
267          {"example.com"}},
268 
269         // One simple horizon case.
270         {BSON("horizon" << matchingHostAndPort),
271          defaultHostAndPort,
272          {{"__default", defaultHostAndPort}, {"horizon", matchingHostAndPort}},
273          ErrorCodes::OK,
274          {},
275          {}},
276 
277         // Two simple horizons case
278         {BSON("horizon" << matchingHostAndPort << "horizon2" << nonmatchingHostAndPort),
279          defaultHostAndPort,
280          {{"__default", defaultHostAndPort},
281           {"horizon", matchingHostAndPort},
282           {"horizon2", nonmatchingHostAndPort}},
283          ErrorCodes::OK,
284          {},
285          {}},
286 
287         // Three horizons, two having duplicate names
288         {
289             BSON("duplicateHorizon"
290                  << "horizon1.example.com:42"
291                  << "duplicateHorizon"
292                  << "horizon2.example.com:42"
293                  << "uniqueHorizon"
294                  << "horizon3.example.com:42"),
295             defaultHostAndPort,
296             {},
297             ErrorCodes::BadValue,
298             {"Duplicate horizon name found", "duplicateHorizon"},
299             {"uniqueHorizon", "__default"}},
300 
301         // Two horizons with duplicate host and ports.
302         {BSON("horizonWithDuplicateHost1" << matchingHostAndPort << "horizonWithDuplicateHost2"
303                                           << matchingHostAndPort
304                                           << "uniqueHorizon"
305                                           << nonmatchingHost),
306          defaultHostAndPort,
307          {},
308          ErrorCodes::BadValue,
309          {"Duplicate horizon member found", matchingHost},
310          {"uniqueHorizon", nonmatchingHost, defaultHost}},
311     };
312 
313     for (const auto& test : tests) {
314         const auto testNumber = &test - tests;
315         const BSONObj bson = BSON("horizons" << test.bsonContents);
316         const auto& expectedErrorCode = test.expectedErrorCode;
317 
318         const auto horizonOpt = [&]() -> boost::optional<SplitHorizon> {
319             const auto host = HostAndPort(test.host);
320             const auto& bsonElement = bson.firstElement();
321             try {
322                 return SplitHorizon(host, bsonElement);
323             } catch (const DBException& ex) {
324                 ASSERT_NOT_EQUALS(expectedErrorCode, ErrorCodes::OK)
325                     << "Failing on test case # " << (&test - tests)
326                     << " with unexpected failure: " << ex.toStatus().reason();
327                 ASSERT_EQUALS(ex.toStatus().code(), expectedErrorCode)
328                     << "Failing status code comparison on test case " << testNumber
329                     << " reason: " << ex.toStatus().reason();
330                 for (const auto& fragment : test.expectedErrorMessageFragments) {
331                     ASSERT_NOT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos)
332                         << "Wanted to see the text fragment \"" << fragment
333                         << "\" in the message: \"" << ex.toStatus().reason() << "\"";
334                 }
335                 for (const auto& fragment : test.absentErrorMessageFragments) {
336                     ASSERT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos);
337                 }
338                 return boost::none;
339             }
340         }();
341 
342         if (!horizonOpt)
343             continue;
344         ASSERT_EQUALS(expectedErrorCode, ErrorCodes::OK);
345 
346         const auto& horizon = *horizonOpt;
347 
348         for (const auto& element : test.expectedMapping) {
349             {
350                 const auto found = horizon.getForwardMappings().find(element.first);
351                 ASSERT_TRUE(found != end(horizon.getForwardMappings()));
352                 ASSERT_EQUALS(HostAndPort(element.second).toString(), found->second.toString());
353             }
354 
355             {
356                 const auto found =
357                     horizon.getReverseHostMappings().find(HostAndPort(element.second).host());
358                 ASSERT_TRUE(found != end(horizon.getReverseHostMappings()))
359                     << "Failed test # " << testNumber
360                     << " because we didn't find a reverse mapping for the host " << element.first;
361                 ASSERT_EQUALS(element.first, found->second) << "on test " << testNumber;
362             }
363         }
364 
365         ASSERT_EQUALS(test.expectedMapping.size(), horizon.getForwardMappings().size());
366         ASSERT_EQUALS(test.expectedMapping.size(), horizon.getReverseHostMappings().size());
367     }
368 }
369 
TEST(SplitHorizonTesting,toBSON)370 TEST(SplitHorizonTesting, toBSON) {
371     struct Input {
372         SplitHorizon::ForwardMapping forwardMapping;  // Will get "__default" added to it.
373 
374         Input(const MappingType& mapping) : forwardMapping(populateForwardMap(mapping)) {}
375     };
376 
377     const Input tests[] = {
378         {{}},
379         {{{"horizon1", "horizon1.example.com:42"}}},
380         {{{"horizon1", "horizon1.example.com:42"}, {"horizon2", "horizon2.example.com:42"}}},
381         {{{"horizon1", "horizon1.example.com:42"},
382           {"horizon2", "horizon2.example.com:42"},
383           {"horizon3", "horizon3.example.com:99"}}},
384     };
385     for (const auto& test : tests) {
386         const auto testNumber = &test - tests;
387         const auto& input = test;
388         const auto& expectedKeys = [&] {
389             auto rv = input.forwardMapping;
390             rv.erase(SplitHorizon::kDefaultHorizon);
391             return rv;
392         }();
393 
394         const SplitHorizon horizon(input.forwardMapping);
395 
396         const BSONObj output = [&] {
397             BSONObjBuilder outputBuilder;
398             horizon.toBSON(outputBuilder);
399             return outputBuilder.obj();
400         }();
401 
402         if (expectedKeys.empty()) {
403             ASSERT_TRUE(output["horizons"].eoo());
404             continue;
405         }
406 
407         ASSERT_FALSE(output["horizons"].eoo());
408 
409         for (const auto& horizons : output) {
410             ASSERT_TRUE(horizons.fieldNameStringData() == "horizons")
411                 << "Test #" << testNumber << " Failing finding a horizons element";
412         }
413 
414         const auto& horizonsElement = output["horizons"];
415         ASSERT_EQUALS(horizonsElement.type(), Object);
416         const auto& horizons = horizonsElement.Obj();
417 
418         std::vector<std::string> visitedHorizons;
419 
420         for (const auto& element : horizons) {
421             ASSERT_TRUE(expectedKeys.count(element.fieldNameStringData().toString()))
422                 << "Test #" << testNumber << " Failing because we found a horizon with the name "
423                 << element.fieldNameStringData() << " which shouldn't exist.";
424 
425             ASSERT_TRUE(
426                 expectedKeys.find(element.fieldNameStringData().toString())->second.toString() ==
427                 element.valueStringData())
428                 << "Test #" << testNumber << " failing because horizon "
429                 << element.fieldNameStringData() << " had an incorrect HostAndPort";
430             visitedHorizons.push_back(element.fieldNameStringData().toString());
431         }
432 
433         ASSERT_EQUALS(visitedHorizons.size(), expectedKeys.size());
434     }
435 }
436 
TEST(SplitHorizonTesting,BSONRoundTrip)437 TEST(SplitHorizonTesting, BSONRoundTrip) {
438     struct Input {
439         SplitHorizon::ForwardMapping forwardMapping;  // Will get "__default" added to it.
440 
441         Input(const MappingType& mapping) : forwardMapping(populateForwardMap(mapping)) {}
442     };
443 
444     const Input tests[] = {
445         {{{"horizon1", "horizon1.example.com:42"}}},
446         {{{"horizon1", "horizon1.example.com:42"}, {"horizon2", "horizon2.example.com:42"}}},
447     };
448     for (const auto& input : tests) {
449         const auto testNumber = &input - tests;
450 
451         const SplitHorizon horizon(input.forwardMapping);
452 
453         const BSONObj bson = [&] {
454             BSONObjBuilder outputBuilder;
455             horizon.toBSON(outputBuilder);
456             return outputBuilder.obj();
457         }();
458 
459         const SplitHorizon witness(HostAndPort(defaultHostAndPort), bson["horizons"]);
460 
461         ASSERT_TRUE(horizon.getForwardMappings() == witness.getForwardMappings())
462             << "Test #" << testNumber << " Failed on bson round trip with forward map";
463         ASSERT_TRUE(horizon.getReverseHostMappings() == witness.getReverseHostMappings())
464             << "Test #" << testNumber << " Failed on bson round trip with reverse map";
465     }
466 }
467 }  // namespace
468 }  // namespace repl
469 }  // namespace mongo
470