1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty.resolver.dns;
17
18 import io.netty.util.NetUtil;
19 import io.netty.util.internal.SocketUtils;
20 import io.netty.util.internal.logging.InternalLogger;
21 import io.netty.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.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT;
37 import static io.netty.util.internal.ObjectUtil.checkNotNull;
38 import static io.netty.util.internal.StringUtil.indexOfNonWhiteSpace;
39 import static io.netty.util.internal.StringUtil.indexOfWhiteSpace;
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(checkNotNull(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<String, DnsServerAddresses>(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<InetSocketAddress>(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 String maybeIP;
191 int x = indexOfWhiteSpace(line, i);
192 if (x == -1) {
193 maybeIP = line.substring(i);
194 } else {
195
196 int idx = indexOfNonWhiteSpace(line, x);
197 if (idx == -1 || line.charAt(idx) != '#') {
198 throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
199 " in file " + etcResolverFile + ". value: " + line);
200 }
201 maybeIP = line.substring(i, x);
202 }
203
204
205 if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) {
206 i = maybeIP.lastIndexOf('.');
207 if (i + 1 >= maybeIP.length()) {
208 throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
209 " in file " + etcResolverFile + ". invalid IP value: " + line);
210 }
211 port = Integer.parseInt(maybeIP.substring(i + 1));
212 maybeIP = maybeIP.substring(0, i);
213 }
214 InetSocketAddress addr = SocketUtils.socketAddress(maybeIP, port);
215
216
217
218
219 if (!addr.isUnresolved()) {
220 addresses.add(addr);
221 }
222 } else if (line.startsWith(DOMAIN_ROW_LABEL)) {
223 int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
224 if (i < 0) {
225 throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL +
226 " in file " + etcResolverFile + " value: " + line);
227 }
228 domainName = line.substring(i);
229 if (!addresses.isEmpty()) {
230 putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate);
231 }
232 addresses = new ArrayList<InetSocketAddress>(2);
233 } else if (line.startsWith(PORT_ROW_LABEL)) {
234 int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length());
235 if (i < 0) {
236 throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL +
237 " in file " + etcResolverFile + " value: " + line);
238 }
239 port = Integer.parseInt(line.substring(i));
240 } else if (line.startsWith(SORTLIST_ROW_LABEL)) {
241 logger.info("row type {} not supported. Ignoring line: {}", SORTLIST_ROW_LABEL, line);
242 }
243 } catch (IllegalArgumentException e) {
244 logger.warn("Could not parse entry. Ignoring line: {}", line, e);
245 }
246 }
247 if (!addresses.isEmpty()) {
248 putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate);
249 }
250 } finally {
251 if (br == null) {
252 fr.close();
253 } else {
254 br.close();
255 }
256 }
257 }
258 return domainToNameServerStreamMap;
259 }
260
261 private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
262 String domainName,
263 List<InetSocketAddress> addresses,
264 boolean rotate) {
265
266 DnsServerAddresses addrs = rotate
267 ? DnsServerAddresses.rotational(addresses)
268 : DnsServerAddresses.sequential(addresses);
269 putIfAbsent(domainToNameServerStreamMap, domainName, addrs);
270 }
271
272 private static void putIfAbsent(Map<String, DnsServerAddresses> domainToNameServerStreamMap,
273 String domainName,
274 DnsServerAddresses addresses) {
275 DnsServerAddresses existingAddresses = domainToNameServerStreamMap.put(domainName, addresses);
276 if (existingAddresses != null) {
277 domainToNameServerStreamMap.put(domainName, existingAddresses);
278 if (logger.isDebugEnabled()) {
279 logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded",
280 domainName, existingAddresses, addresses);
281 }
282 }
283 }
284
285
286
287
288
289
290
291 static UnixResolverOptions parseEtcResolverOptions() throws IOException {
292 return parseEtcResolverOptions(new File(ETC_RESOLV_CONF_FILE));
293 }
294
295
296
297
298
299
300
301
302 static UnixResolverOptions parseEtcResolverOptions(File etcResolvConf) throws IOException {
303 UnixResolverOptions.Builder optionsBuilder = UnixResolverOptions.newBuilder();
304
305 FileReader fr = new FileReader(etcResolvConf);
306 BufferedReader br = null;
307 try {
308 br = new BufferedReader(fr);
309 String line;
310 while ((line = br.readLine()) != null) {
311 if (line.startsWith(OPTIONS_ROW_LABEL)) {
312 parseResOptions(line.substring(OPTIONS_ROW_LABEL.length()), optionsBuilder);
313 break;
314 }
315 }
316 } finally {
317 if (br == null) {
318 fr.close();
319 } else {
320 br.close();
321 }
322 }
323
324
325 if (RES_OPTIONS != null) {
326 parseResOptions(RES_OPTIONS, optionsBuilder);
327 }
328
329 return optionsBuilder.build();
330 }
331
332 private static void parseResOptions(String line, UnixResolverOptions.Builder builder) {
333 String[] opts = WHITESPACE_PATTERN.split(line);
334 for (String opt : opts) {
335 try {
336 if (opt.startsWith("ndots:")) {
337 builder.setNdots(parseResIntOption(opt, "ndots:"));
338 } else if (opt.startsWith("attempts:")) {
339 builder.setAttempts(parseResIntOption(opt, "attempts:"));
340 } else if (opt.startsWith("timeout:")) {
341 builder.setTimeout(parseResIntOption(opt, "timeout:"));
342 }
343 } catch (NumberFormatException ignore) {
344
345 }
346 }
347 }
348
349 private static int parseResIntOption(String opt, String fullLabel) {
350 String optValue = opt.substring(fullLabel.length());
351 return Integer.parseInt(optValue);
352 }
353
354
355
356
357
358
359
360 static List<String> parseEtcResolverSearchDomains() throws IOException {
361 return parseEtcResolverSearchDomains(new File(ETC_RESOLV_CONF_FILE));
362 }
363
364
365
366
367
368
369
370
371 static List<String> parseEtcResolverSearchDomains(File etcResolvConf) throws IOException {
372 String localDomain = null;
373 List<String> searchDomains = new ArrayList<String>();
374
375 FileReader fr = new FileReader(etcResolvConf);
376 BufferedReader br = null;
377 try {
378 br = new BufferedReader(fr);
379 String line;
380 while ((line = br.readLine()) != null) {
381 if (localDomain == null && line.startsWith(DOMAIN_ROW_LABEL)) {
382 int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
383 if (i >= 0) {
384 localDomain = line.substring(i);
385 }
386 } else if (line.startsWith(SEARCH_ROW_LABEL)) {
387 int i = indexOfNonWhiteSpace(line, SEARCH_ROW_LABEL.length());
388 if (i >= 0) {
389
390
391 String[] domains = WHITESPACE_PATTERN.split(line.substring(i));
392 Collections.addAll(searchDomains, domains);
393 }
394 }
395 }
396 } finally {
397 if (br == null) {
398 fr.close();
399 } else {
400 br.close();
401 }
402 }
403
404
405 return localDomain != null && searchDomains.isEmpty()
406 ? Collections.singletonList(localDomain)
407 : searchDomains;
408 }
409
410 }