1 // Copyright 2021 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/dns/nsswitch_reader.h"
6
7 #include <string>
8 #include <utility>
9 #include <vector>
10
11 #include "base/files/file_path.h"
12 #include "base/files/file_util.h"
13 #include "base/functional/bind.h"
14 #include "base/functional/callback.h"
15 #include "base/metrics/histogram_macros.h"
16 #include "base/strings/string_piece.h"
17 #include "base/strings/string_split.h"
18 #include "base/strings/string_util.h"
19 #include "build/build_config.h"
20
21 #if BUILDFLAG(IS_POSIX)
22 #include <netdb.h>
23 #endif // defined (OS_POSIX)
24
25 namespace net {
26
27 namespace {
28
29 #ifdef _PATH_NSSWITCH_CONF
30 constexpr base::FilePath::CharType kNsswitchPath[] =
31 FILE_PATH_LITERAL(_PATH_NSSWITCH_CONF);
32 #else
33 constexpr base::FilePath::CharType kNsswitchPath[] =
34 FILE_PATH_LITERAL("/etc/nsswitch.conf");
35 #endif
36
37 // Choose 1 MiB as the largest handled filesize. Arbitrarily chosen as seeming
38 // large enough to handle any reasonable file contents and similar to the size
39 // limit for HOSTS files (32 MiB).
40 constexpr size_t kMaxFileSize = 1024 * 1024;
41
ReadNsswitch()42 std::string ReadNsswitch() {
43 std::string file;
44 bool result = base::ReadFileToStringWithMaxSize(base::FilePath(kNsswitchPath),
45 &file, kMaxFileSize);
46 UMA_HISTOGRAM_BOOLEAN("Net.DNS.DnsConfig.Nsswitch.Read",
47 result || file.size() == kMaxFileSize);
48 UMA_HISTOGRAM_BOOLEAN("Net.DNS.DnsConfig.Nsswitch.TooLarge",
49 !result && file.size() == kMaxFileSize);
50
51 if (result)
52 return file;
53
54 return "";
55 }
56
SkipRestOfLine(base::StringPiece text)57 base::StringPiece SkipRestOfLine(base::StringPiece text) {
58 base::StringPiece::size_type line_end = text.find('\n');
59 if (line_end == base::StringPiece::npos)
60 return "";
61 return text.substr(line_end);
62 }
63
64 // In case of multiple entries for `database_name`, finds only the first.
FindDatabase(base::StringPiece text,base::StringPiece database_name)65 base::StringPiece FindDatabase(base::StringPiece text,
66 base::StringPiece database_name) {
67 DCHECK(!text.empty());
68 DCHECK(!database_name.empty());
69 DCHECK(!base::StartsWith(database_name, "#"));
70 DCHECK(!base::IsAsciiWhitespace(database_name.front()));
71 DCHECK(base::EndsWith(database_name, ":"));
72
73 while (!text.empty()) {
74 text = base::TrimWhitespaceASCII(text, base::TrimPositions::TRIM_LEADING);
75
76 if (base::StartsWith(text, database_name,
77 base::CompareCase::INSENSITIVE_ASCII)) {
78 DCHECK(!base::StartsWith(text, "#"));
79
80 text = text.substr(database_name.size());
81 base::StringPiece::size_type line_end = text.find('\n');
82 if (line_end != base::StringPiece::npos)
83 text = text.substr(0, line_end);
84
85 return base::TrimWhitespaceASCII(text, base::TrimPositions::TRIM_ALL);
86 }
87
88 text = SkipRestOfLine(text);
89 }
90
91 return "";
92 }
93
TokenizeAction(base::StringPiece action_column)94 NsswitchReader::ServiceAction TokenizeAction(base::StringPiece action_column) {
95 DCHECK(!action_column.empty());
96 DCHECK_EQ(action_column.find(']'), base::StringPiece::npos);
97 DCHECK_EQ(action_column.find_first_of(base::kWhitespaceASCII),
98 base::StringPiece::npos);
99
100 NsswitchReader::ServiceAction result = {/*negated=*/false,
101 NsswitchReader::Status::kUnknown,
102 NsswitchReader::Action::kUnknown};
103
104 std::vector<base::StringPiece> split = base::SplitStringPiece(
105 action_column, "=", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
106 if (split.size() != 2)
107 return result;
108
109 if (split[0].size() >= 2 && split[0].front() == '!') {
110 result.negated = true;
111 split[0] = split[0].substr(1);
112 }
113
114 if (base::EqualsCaseInsensitiveASCII(split[0], "SUCCESS")) {
115 result.status = NsswitchReader::Status::kSuccess;
116 } else if (base::EqualsCaseInsensitiveASCII(split[0], "NOTFOUND")) {
117 result.status = NsswitchReader::Status::kNotFound;
118 } else if (base::EqualsCaseInsensitiveASCII(split[0], "UNAVAIL")) {
119 result.status = NsswitchReader::Status::kUnavailable;
120 } else if (base::EqualsCaseInsensitiveASCII(split[0], "TRYAGAIN")) {
121 result.status = NsswitchReader::Status::kTryAgain;
122 }
123
124 if (base::EqualsCaseInsensitiveASCII(split[1], "RETURN")) {
125 result.action = NsswitchReader::Action::kReturn;
126 } else if (base::EqualsCaseInsensitiveASCII(split[1], "CONTINUE")) {
127 result.action = NsswitchReader::Action::kContinue;
128 } else if (base::EqualsCaseInsensitiveASCII(split[1], "MERGE")) {
129 result.action = NsswitchReader::Action::kMerge;
130 }
131
132 return result;
133 }
134
TokenizeActions(base::StringPiece actions)135 std::vector<NsswitchReader::ServiceAction> TokenizeActions(
136 base::StringPiece actions) {
137 DCHECK(!actions.empty());
138 DCHECK_NE(actions.front(), '[');
139 DCHECK_EQ(actions.find(']'), base::StringPiece::npos);
140 DCHECK(!base::IsAsciiWhitespace(actions.front()));
141
142 std::vector<NsswitchReader::ServiceAction> result;
143
144 for (const auto& action_column : base::SplitStringPiece(
145 actions, base::kWhitespaceASCII, base::KEEP_WHITESPACE,
146 base::SPLIT_WANT_NONEMPTY)) {
147 DCHECK(!action_column.empty());
148 result.push_back(TokenizeAction(action_column));
149 }
150
151 return result;
152 }
153
TokenizeService(base::StringPiece service_column)154 NsswitchReader::ServiceSpecification TokenizeService(
155 base::StringPiece service_column) {
156 DCHECK(!service_column.empty());
157 DCHECK_EQ(service_column.find_first_of(base::kWhitespaceASCII),
158 base::StringPiece::npos);
159 DCHECK_NE(service_column.front(), '[');
160
161 if (base::EqualsCaseInsensitiveASCII(service_column, "files")) {
162 return NsswitchReader::ServiceSpecification(
163 NsswitchReader::Service::kFiles);
164 }
165 if (base::EqualsCaseInsensitiveASCII(service_column, "dns")) {
166 return NsswitchReader::ServiceSpecification(NsswitchReader::Service::kDns);
167 }
168 if (base::EqualsCaseInsensitiveASCII(service_column, "mdns")) {
169 return NsswitchReader::ServiceSpecification(NsswitchReader::Service::kMdns);
170 }
171 if (base::EqualsCaseInsensitiveASCII(service_column, "mdns4")) {
172 return NsswitchReader::ServiceSpecification(
173 NsswitchReader::Service::kMdns4);
174 }
175 if (base::EqualsCaseInsensitiveASCII(service_column, "mdns6")) {
176 return NsswitchReader::ServiceSpecification(
177 NsswitchReader::Service::kMdns6);
178 }
179 if (base::EqualsCaseInsensitiveASCII(service_column, "mdns_minimal")) {
180 return NsswitchReader::ServiceSpecification(
181 NsswitchReader::Service::kMdnsMinimal);
182 }
183 if (base::EqualsCaseInsensitiveASCII(service_column, "mdns4_minimal")) {
184 return NsswitchReader::ServiceSpecification(
185 NsswitchReader::Service::kMdns4Minimal);
186 }
187 if (base::EqualsCaseInsensitiveASCII(service_column, "mdns6_minimal")) {
188 return NsswitchReader::ServiceSpecification(
189 NsswitchReader::Service::kMdns6Minimal);
190 }
191 if (base::EqualsCaseInsensitiveASCII(service_column, "myhostname")) {
192 return NsswitchReader::ServiceSpecification(
193 NsswitchReader::Service::kMyHostname);
194 }
195 if (base::EqualsCaseInsensitiveASCII(service_column, "resolve")) {
196 return NsswitchReader::ServiceSpecification(
197 NsswitchReader::Service::kResolve);
198 }
199 if (base::EqualsCaseInsensitiveASCII(service_column, "nis")) {
200 return NsswitchReader::ServiceSpecification(NsswitchReader::Service::kNis);
201 }
202
203 return NsswitchReader::ServiceSpecification(
204 NsswitchReader::Service::kUnknown);
205 }
206
207 // Returns the actions string without brackets. `out_num_bytes` returns number
208 // of bytes in the actions including brackets and trailing whitespace.
GetActionsStringAndRemoveBrackets(base::StringPiece database,size_t & out_num_bytes)209 base::StringPiece GetActionsStringAndRemoveBrackets(base::StringPiece database,
210 size_t& out_num_bytes) {
211 DCHECK(!database.empty());
212 DCHECK_EQ(database.front(), '[');
213
214 size_t action_end = database.find(']');
215
216 base::StringPiece actions;
217 if (action_end == base::StringPiece::npos) {
218 actions = database.substr(1);
219 out_num_bytes = database.size();
220 } else {
221 actions = database.substr(1, action_end - 1);
222 out_num_bytes = action_end;
223 }
224
225 // Ignore repeated '[' at start of `actions`.
226 actions =
227 base::TrimWhitespaceASCII(actions, base::TrimPositions::TRIM_LEADING);
228 while (!actions.empty() && actions.front() == '[') {
229 actions = base::TrimWhitespaceASCII(actions.substr(1),
230 base::TrimPositions::TRIM_LEADING);
231 }
232
233 // Include any trailing ']' and whitespace in `out_num_bytes`.
234 while (out_num_bytes < database.size() &&
235 (database[out_num_bytes] == ']' ||
236 base::IsAsciiWhitespace(database[out_num_bytes]))) {
237 ++out_num_bytes;
238 }
239
240 return actions;
241 }
242
TokenizeDatabase(base::StringPiece database)243 std::vector<NsswitchReader::ServiceSpecification> TokenizeDatabase(
244 base::StringPiece database) {
245 std::vector<NsswitchReader::ServiceSpecification> tokenized;
246
247 while (!database.empty()) {
248 DCHECK(!base::IsAsciiWhitespace(database.front()));
249
250 // Note: Assuming comments are not recognized mid-action or mid-service.
251 if (database.front() == '#') {
252 // Once a comment is hit, the rest of the database is comment.
253 return tokenized;
254 }
255
256 if (database.front() == '[') {
257 // Actions are expected to come after a service.
258 if (tokenized.empty()) {
259 tokenized.emplace_back(NsswitchReader::Service::kUnknown);
260 }
261
262 size_t num_actions_bytes = 0;
263 base::StringPiece actions =
264 GetActionsStringAndRemoveBrackets(database, num_actions_bytes);
265
266 if (num_actions_bytes == database.size()) {
267 database = "";
268 } else {
269 database = database.substr(num_actions_bytes);
270 }
271
272 if (!actions.empty()) {
273 std::vector<NsswitchReader::ServiceAction> tokenized_actions =
274 TokenizeActions(actions);
275 tokenized.back().actions.insert(tokenized.back().actions.end(),
276 tokenized_actions.begin(),
277 tokenized_actions.end());
278 }
279 } else {
280 size_t column_end = database.find_first_of(base::kWhitespaceASCII);
281
282 base::StringPiece service_column;
283 if (column_end == base::StringPiece::npos) {
284 service_column = database;
285 database = "";
286 } else {
287 service_column = database.substr(0, column_end);
288 database = database.substr(column_end);
289 }
290
291 tokenized.push_back(TokenizeService(service_column));
292 }
293
294 database =
295 base::TrimWhitespaceASCII(database, base::TrimPositions::TRIM_LEADING);
296 }
297
298 return tokenized;
299 }
300
GetDefaultHosts()301 std::vector<NsswitchReader::ServiceSpecification> GetDefaultHosts() {
302 return {NsswitchReader::ServiceSpecification(NsswitchReader::Service::kFiles),
303 NsswitchReader::ServiceSpecification(NsswitchReader::Service::kDns)};
304 }
305
306 } // namespace
307
ServiceSpecification(Service service,std::vector<ServiceAction> actions)308 NsswitchReader::ServiceSpecification::ServiceSpecification(
309 Service service,
310 std::vector<ServiceAction> actions)
311 : service(service), actions(std::move(actions)) {}
312
313 NsswitchReader::ServiceSpecification::~ServiceSpecification() = default;
314
315 NsswitchReader::ServiceSpecification::ServiceSpecification(
316 const ServiceSpecification&) = default;
317
318 NsswitchReader::ServiceSpecification&
319 NsswitchReader::ServiceSpecification::operator=(const ServiceSpecification&) =
320 default;
321
322 NsswitchReader::ServiceSpecification::ServiceSpecification(
323 ServiceSpecification&&) = default;
324
325 NsswitchReader::ServiceSpecification&
326 NsswitchReader::ServiceSpecification::operator=(ServiceSpecification&&) =
327 default;
328
NsswitchReader()329 NsswitchReader::NsswitchReader()
330 : file_read_call_(base::BindRepeating(&ReadNsswitch)) {}
331
332 NsswitchReader::~NsswitchReader() = default;
333
334 std::vector<NsswitchReader::ServiceSpecification>
ReadAndParseHosts()335 NsswitchReader::ReadAndParseHosts() {
336 std::string file = file_read_call_.Run();
337 if (file.empty())
338 return GetDefaultHosts();
339
340 base::StringPiece hosts = FindDatabase(file, "hosts:");
341 UMA_HISTOGRAM_BOOLEAN("Net.DNS.DnsConfig.Nsswitch.HostsFound",
342 !hosts.empty());
343 if (hosts.empty())
344 return GetDefaultHosts();
345
346 return TokenizeDatabase(hosts);
347 }
348
349 } // namespace net
350