1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty5.resolver.dns;
17
18 import io.netty5.util.NetUtil;
19 import io.netty5.util.internal.SocketUtils;
20 import io.netty5.util.internal.logging.InternalLogger;
21 import io.netty5.util.internal.logging.InternalLoggerFactory;
22
23 import java.io.BufferedReader;
24 import java.io.File;
25 import java.io.FileReader;
26 import java.io.IOException;
27 import java.net.InetSocketAddress;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.regex.Pattern;
35
36 import static io.netty5.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT;
37 import static io.netty5.util.internal.StringUtil.indexOfNonWhiteSpace;
38 import static io.netty5.util.internal.StringUtil.indexOfWhiteSpace;
39 import static java.util.Objects.requireNonNull;
40
41
42
43
44
45
46 public final class UnixResolverDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider {
47 private static final InternalLogger logger =
48 InternalLoggerFactory.getInstance(UnixResolverDnsServerAddressStreamProvider.class);
49
50 private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
51 private static final String RES_OPTIONS = System.getenv("RES_OPTIONS");
52
53 private static final String ETC_RESOLV_CONF_FILE = "/etc/resolv.conf";
54 private static final String ETC_RESOLVER_DIR = "/etc/resolver";
55 private static final String NAMESERVER_ROW_LABEL = "nameserver";
56 private static final String SORTLIST_ROW_LABEL = "sortlist";
57 private static final String OPTIONS_ROW_LABEL = "options ";
58 private static final String OPTIONS_ROTATE_FLAG = "rotate";
59 private static final String DOMAIN_ROW_LABEL = "domain";
60 private static final String SEARCH_ROW_LABEL = "search";
61 private static final String PORT_ROW_LABEL = "port";
62
63 private final DnsServerAddresses defaultNameServerAddresses;
64 private final Map<String, DnsServerAddresses> domainToNameServerStreamMap;
65
66
67
68
69
70 static DnsServerAddressStreamProvider parseSilently() {
71 try {
72 UnixResolverDnsServerAddressStreamProvider nameServerCache =
73 new UnixResolverDnsServerAddressStreamProvider(ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR);
74 return nameServerCache.mayOverrideNameServers() ? nameServerCache
75 : DefaultDnsServerAddressStreamProvider.INSTANCE;
76 } catch (Exception e) {
77 if (logger.isDebugEnabled()) {
78 logger.debug("failed to parse {} and/or {}", ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR, e);
79 }
80 return DefaultDnsServerAddressStreamProvider.INSTANCE;
81 }
82 }
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97 public UnixResolverDnsServerAddressStreamProvider(File etcResolvConf, File... etcResolverFiles) throws IOException {
98 Map<String, DnsServerAddresses> etcResolvConfMap = parse(requireNonNull(etcResolvConf, "etcResolvConf"));
99 final boolean useEtcResolverFiles = etcResolverFiles != null && etcResolverFiles.length != 0;
100 domainToNameServerStreamMap = useEtcResolverFiles ? parse(etcResolverFiles) : etcResolvConfMap;
101
102 DnsServerAddresses defaultNameServerAddresses
103 = etcResolvConfMap.get(etcResolvConf.getName());
104 if (defaultNameServerAddresses == null) {
105 Collection<DnsServerAddresses> values = etcResolvConfMap.values();
106 if (values.isEmpty()) {
107 throw new IllegalArgumentException(etcResolvConf + " didn't provide any name servers");
108 }
109 this.defaultNameServerAddresses = values.iterator().next();
110 } else {
111 this.defaultNameServerAddresses = defaultNameServerAddresses;
112 }
113
114 if (useEtcResolverFiles) {
115 domainToNameServerStreamMap.putAll(etcResolvConfMap);
116 }
117 }
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132 public UnixResolverDnsServerAddressStreamProvider(String etcResolvConf, String etcResolverDir) throws IOException {
133 this(etcResolvConf == null ? null : new File(etcResolvConf),
134 etcResolverDir == null ? null : new File(etcResolverDir).listFiles());
135 }
136
137 @Override
138 public DnsServerAddressStream nameServerAddressStream(String hostname) {
139 for (;;) {
140 int i = hostname.indexOf('.', 1);
141 if (i < 0 || i == hostname.length() - 1) {
142 return defaultNameServerAddresses.stream();
143 }
144
145 DnsServerAddresses addresses = domainToNameServerStreamMap.get(hostname);
146 if (addresses != null) {
147 return addresses.stream();
148 }
149
150 hostname = hostname.substring(i + 1);
151 }
152 }
153
154 private boolean mayOverrideNameServers() {
155 return !domainToNameServerStreamMap.isEmpty() || defaultNameServerAddresses.stream().next() != null;
156 }
157
158 private static Map<String, DnsServerAddresses> parse(File... etcResolverFiles) throws IOException {
159 Map<String, DnsServerAddresses> domainToNameServerStreamMap =
160 new HashMap<>(etcResolverFiles.length << 1);
161 boolean rotateGlobal = RES_OPTIONS != null && RES_OPTIONS.contains(OPTIONS_ROTATE_FLAG);
162 for (File etcResolverFile : etcResolverFiles) {
163 if (!etcResolverFile.isFile()) {
164 continue;
165 }
166 FileReader fr = new FileReader(etcResolverFile);
167 BufferedReader br = null;
168 try {
169 br = new BufferedReader(fr);
170 List<InetSocketAddress> addresses = new ArrayList<>(2);
171 String domainName = etcResolverFile.getName();
172 boolean rotate = rotateGlobal;
173 int port = DNS_PORT;
174 String line;
175 while ((line = br.readLine()) != null) {
176 line = line.trim();
177 try {
178 char c;
179 if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') {
180 continue;
181 }
182 if (!rotate && line.startsWith(OPTIONS_ROW_LABEL)) {
183 rotate = line.contains(OPTIONS_ROTATE_FLAG);
184 } else if (line.startsWith(NAMESERVER_ROW_LABEL)) {
185 int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length());
186 if (i < 0) {
187 throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
188 " in file " + etcResolverFile + ". value: " + line);
189 }
190
191 String maybeIP;
192 int x = indexOfWhiteSpace(line, i);
193 if (x == -1) {
194 maybeIP = line.substring(i);
195 } else {
196
197 int idx = indexOfNonWhiteSpace(line, x);
198 if (idx == -1 || line.charAt(idx) != '#') {
199 throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
200 " in file " + etcResolverFile + ". value: " + line);
201 }
202 maybeIP = line.substring(i, x);
203 }
204
205
206 if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) {
207 i = maybeIP.lastIndexOf('.');
208 if (i + 1 >= maybeIP.length()) {
209 throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
210 " in file " + etcResolverFile + ". invalid IP value: " + line);
211 }
212 port = Integer.parseInt(maybeIP.substring(i + 1));
213 maybeIP = maybeIP.substring(0, i);
214 }
215 addresses.add(SocketUtils.socketAddress(maybeIP, port));
216 } else if (line.startsWith(DOMAIN_ROW_LABEL)) {
217 int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
218 if (i < 0) {
219 throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL +
220 " in file " + etcResolverFile + " value: " + line);
221 }
222 domainName = line.substring(i);
223 if (!addresses.isEmpty()) {
224 putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate);
225 }
226 addresses = new ArrayList<>(2);
227 } else if (line.startsWith(PORT_ROW_LABEL)) {
228 int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length());
229 if (i < 0) {
230 throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL +
231 " in file " + etcResolverFile + " value: " + line);
232 }
233 port = Integer.parseInt(line.substring(i));
234 } else if (line.startsWith(SORTLIST_ROW_LABEL)) {
235 logger.info("row type {} not supported. Ignoring line: {}", SORTLIST_ROW_LABEL, line);
236 }
237 } catch (IllegalArgumentException e) {
238 logger.warn("Could not parse entry. Ignoring line: {}", line, e);
239 }
240 }
241 if (!addresses.isEmpty()) {
242 putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate);
243 }
244 } finally {
245 if (br == null) {
246 fr.close();
247 } else {
248 br.close();
249 }
250 }
251 }
252 return domainToNameServerStreamMap;
253 }
254
255 private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
256 String domainName,
257 List<InetSocketAddress> addresses,
258 boolean rotate) {
259
260 DnsServerAddresses addrs = rotate
261 ? DnsServerAddresses.rotational(addresses)
262 : DnsServerAddresses.sequential(addresses);
263 putIfAbsent(domainToNameServerStreamMap, domainName, addrs);
264 }
265
266 private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
267 String domainName,
268 DnsServerAddresses addresses) {
269 DnsServerAddresses existingAddresses = domainToNameServerStreamMap.put(domainName, addresses);
270 if (existingAddresses != null) {
271 domainToNameServerStreamMap.put(domainName, existingAddresses);
272 if (logger.isDebugEnabled()) {
273 logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded",
274 domainName, existingAddresses, addresses);
275 }
276 }
277 }
278
279
280
281
282
283
284
285 static UnixResolverOptions parseEtcResolverOptions() throws IOException {
286 return parseEtcResolverOptions(new File(ETC_RESOLV_CONF_FILE));
287 }
288
289
290
291
292
293
294
295
296 static UnixResolverOptions parseEtcResolverOptions(File etcResolvConf) throws IOException {
297 UnixResolverOptions.Builder optionsBuilder = UnixResolverOptions.newBuilder();
298
299 FileReader fr = new FileReader(etcResolvConf);
300 BufferedReader br = null;
301 try {
302 br = new BufferedReader(fr);
303 String line;
304 while ((line = br.readLine()) != null) {
305 if (line.startsWith(OPTIONS_ROW_LABEL)) {
306 parseResOptions(line.substring(OPTIONS_ROW_LABEL.length()), optionsBuilder);
307 break;
308 }
309 }
310 } finally {
311 if (br == null) {
312 fr.close();
313 } else {
314 br.close();
315 }
316 }
317
318
319 if (RES_OPTIONS != null) {
320 parseResOptions(RES_OPTIONS, optionsBuilder);
321 }
322
323 return optionsBuilder.build();
324 }
325
326 private static void parseResOptions(String line, UnixResolverOptions.Builder builder) {
327 String[] opts = WHITESPACE_PATTERN.split(line);
328 for (String opt : opts) {
329 try {
330 if (opt.startsWith("ndots:")) {
331 builder.setNdots(parseResIntOption(opt, "ndots:"));
332 } else if (opt.startsWith("attempts:")) {
333 builder.setAttempts(parseResIntOption(opt, "attempts:"));
334 } else if (opt.startsWith("timeout:")) {
335 builder.setTimeout(parseResIntOption(opt, "timeout:"));
336 }
337 } catch (NumberFormatException ignore) {
338
339 }
340 }
341 }
342
343 private static int parseResIntOption(String opt, String fullLabel) {
344 String optValue = opt.substring(fullLabel.length());
345 return Integer.parseInt(optValue);
346 }
347
348
349
350
351
352
353
354 static List<String> parseEtcResolverSearchDomains() throws IOException {
355 return parseEtcResolverSearchDomains(new File(ETC_RESOLV_CONF_FILE));
356 }
357
358
359
360
361
362
363
364
365 static List<String> parseEtcResolverSearchDomains(File etcResolvConf) throws IOException {
366 String localDomain = null;
367 List<String> searchDomains = new ArrayList<>();
368
369 FileReader fr = new FileReader(etcResolvConf);
370 BufferedReader br = null;
371 try {
372 br = new BufferedReader(fr);
373 String line;
374 while ((line = br.readLine()) != null) {
375 if (localDomain == null && line.startsWith(DOMAIN_ROW_LABEL)) {
376 int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
377 if (i >= 0) {
378 localDomain = line.substring(i);
379 }
380 } else if (line.startsWith(SEARCH_ROW_LABEL)) {
381 int i = indexOfNonWhiteSpace(line, SEARCH_ROW_LABEL.length());
382 if (i >= 0) {
383
384
385 String[] domains = WHITESPACE_PATTERN.split(line.substring(i));
386 Collections.addAll(searchDomains, domains);
387 }
388 }
389 }
390 } finally {
391 if (br == null) {
392 fr.close();
393 } else {
394 br.close();
395 }
396 }
397
398
399 return localDomain != null && searchDomains.isEmpty()
400 ? Collections.singletonList(localDomain)
401 : searchDomains;
402 }
403
404 }