1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package io.netty.resolver.dns;
18
19 import io.netty.buffer.ByteBuf;
20 import io.netty.channel.socket.InternetProtocolFamily;
21 import io.netty.handler.codec.dns.DnsClass;
22 import io.netty.handler.codec.dns.DnsQuestion;
23 import io.netty.handler.codec.dns.DnsResource;
24 import io.netty.handler.codec.dns.DnsResponse;
25 import io.netty.handler.codec.dns.DnsResponseDecoder;
26 import io.netty.handler.codec.dns.DnsType;
27 import io.netty.util.CharsetUtil;
28 import io.netty.util.ReferenceCountUtil;
29 import io.netty.util.concurrent.Future;
30 import io.netty.util.concurrent.FutureListener;
31 import io.netty.util.concurrent.Promise;
32 import io.netty.util.internal.StringUtil;
33
34 import java.net.Inet4Address;
35 import java.net.Inet6Address;
36 import java.net.InetAddress;
37 import java.net.InetSocketAddress;
38 import java.net.UnknownHostException;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.HashMap;
42 import java.util.IdentityHashMap;
43 import java.util.Iterator;
44 import java.util.List;
45 import java.util.Locale;
46 import java.util.Map;
47 import java.util.Set;
48
49 final class DnsNameResolverContext {
50
51 private static final int INADDRSZ4 = 4;
52 private static final int INADDRSZ6 = 16;
53
54 private static final FutureListener<DnsResponse> RELEASE_RESPONSE = new FutureListener<DnsResponse>() {
55 @Override
56 public void operationComplete(Future<DnsResponse> future) {
57 if (future.isSuccess()) {
58 future.getNow().release();
59 }
60 }
61 };
62
63 private final DnsNameResolver parent;
64 private final Promise<InetSocketAddress> promise;
65 private final String hostname;
66 private final int port;
67 private final int maxAllowedQueries;
68 private final InternetProtocolFamily[] resolveAddressTypes;
69
70 private final Set<Future<DnsResponse>> queriesInProgress =
71 Collections.newSetFromMap(new IdentityHashMap<Future<DnsResponse>, Boolean>());
72 private List<InetAddress> resolvedAddresses;
73 private StringBuilder trace;
74 private int allowedQueries;
75 private boolean triedCNAME;
76
77 DnsNameResolverContext(DnsNameResolver parent, String hostname, int port, Promise<InetSocketAddress> promise) {
78 this.parent = parent;
79 this.promise = promise;
80 this.hostname = hostname;
81 this.port = port;
82
83 maxAllowedQueries = parent.maxQueriesPerResolve();
84 resolveAddressTypes = parent.resolveAddressTypesUnsafe();
85 allowedQueries = maxAllowedQueries;
86 }
87
88 void resolve() {
89 for (InternetProtocolFamily f: resolveAddressTypes) {
90 final DnsType type;
91 switch (f) {
92 case IPv4:
93 type = DnsType.A;
94 break;
95 case IPv6:
96 type = DnsType.AAAA;
97 break;
98 default:
99 throw new Error();
100 }
101
102 query(parent.nameServerAddresses, new DnsQuestion(hostname, type));
103 }
104 }
105
106 private void query(Iterable<InetSocketAddress> nameServerAddresses, final DnsQuestion question) {
107 if (allowedQueries == 0 || promise.isCancelled()) {
108 return;
109 }
110
111 allowedQueries --;
112
113 final Future<DnsResponse> f = parent.query(nameServerAddresses, question);
114 queriesInProgress.add(f);
115
116 f.addListener(new FutureListener<DnsResponse>() {
117 @Override
118 public void operationComplete(Future<DnsResponse> future) throws Exception {
119 queriesInProgress.remove(future);
120
121 if (promise.isDone()) {
122 return;
123 }
124
125 try {
126 if (future.isSuccess()) {
127 onResponse(question, future.getNow());
128 } else {
129 addTrace(future.cause());
130 }
131 } finally {
132 tryToFinishResolve();
133 }
134 }
135 });
136 }
137
138 void onResponse(final DnsQuestion question, final DnsResponse response) {
139 final DnsType type = question.type();
140 try {
141 if (type == DnsType.A || type == DnsType.AAAA) {
142 onResponseAorAAAA(type, question, response);
143 } else if (type == DnsType.CNAME) {
144 onResponseCNAME(question, response);
145 }
146 } finally {
147 ReferenceCountUtil.safeRelease(response);
148 }
149 }
150
151 private void onResponseAorAAAA(DnsType qType, DnsQuestion question, DnsResponse response) {
152
153 final Map<String, String> cnames = buildAliasMap(response);
154
155 boolean found = false;
156 for (DnsResource r: response.answers()) {
157 final DnsType type = r.type();
158 if (type != DnsType.A && type != DnsType.AAAA) {
159 continue;
160 }
161
162 final String qName = question.name().toLowerCase(Locale.US);
163 final String rName = r.name().toLowerCase(Locale.US);
164
165
166 if (!rName.equals(qName)) {
167
168 String resolved = qName;
169 do {
170 resolved = cnames.get(resolved);
171 if (rName.equals(resolved)) {
172 break;
173 }
174 } while (resolved != null);
175
176 if (resolved == null) {
177 continue;
178 }
179 }
180
181 final ByteBuf content = r.content();
182 final int contentLen = content.readableBytes();
183 if (contentLen != INADDRSZ4 && contentLen != INADDRSZ6) {
184 continue;
185 }
186
187 final byte[] addrBytes = new byte[contentLen];
188 content.getBytes(content.readerIndex(), addrBytes);
189
190 try {
191 InetAddress resolved = InetAddress.getByAddress(hostname, addrBytes);
192 if (resolvedAddresses == null) {
193 resolvedAddresses = new ArrayList<InetAddress>();
194 }
195 resolvedAddresses.add(resolved);
196 found = true;
197 } catch (UnknownHostException e) {
198
199 throw new Error(e);
200 }
201 }
202
203 if (found) {
204 return;
205 }
206
207 addTrace(response.sender(), "no matching " + qType + " record found");
208
209
210 if (!cnames.isEmpty()) {
211 onResponseCNAME(question, response, cnames, false);
212 }
213 }
214
215 private void onResponseCNAME(DnsQuestion question, DnsResponse response) {
216 onResponseCNAME(question, response, buildAliasMap(response), true);
217 }
218
219 private void onResponseCNAME(
220 DnsQuestion question, DnsResponse response, Map<String, String> cnames, boolean trace) {
221
222
223 final String name = question.name().toLowerCase(Locale.US);
224 String resolved = name;
225 boolean found = false;
226 for (;;) {
227 String next = cnames.get(resolved);
228 if (next != null) {
229 found = true;
230 resolved = next;
231 } else {
232 break;
233 }
234 }
235
236 if (found) {
237 followCname(response.sender(), name, resolved);
238 } else if (trace) {
239 addTrace(response.sender(), "no matching CNAME record found");
240 }
241 }
242
243 private static Map<String, String> buildAliasMap(DnsResponse response) {
244 Map<String, String> cnames = null;
245 for (DnsResource r: response.answers()) {
246 final DnsType type = r.type();
247 if (type != DnsType.CNAME) {
248 continue;
249 }
250
251 String content = decodeDomainName(r.content());
252 if (content == null) {
253 continue;
254 }
255
256 if (cnames == null) {
257 cnames = new HashMap<String, String>();
258 }
259
260 cnames.put(r.name().toLowerCase(Locale.US), content.toLowerCase(Locale.US));
261 }
262
263 return cnames != null? cnames : Collections.<String, String>emptyMap();
264 }
265
266 void tryToFinishResolve() {
267 if (!queriesInProgress.isEmpty()) {
268
269 if (gotPreferredAddress()) {
270
271 finishResolve();
272 }
273
274
275 return;
276 }
277
278
279 if (resolvedAddresses == null) {
280
281 if (!triedCNAME) {
282
283 triedCNAME = true;
284 query(parent.nameServerAddresses, new DnsQuestion(hostname, DnsType.CNAME, DnsClass.IN));
285 return;
286 }
287 }
288
289
290 finishResolve();
291 }
292
293 private boolean gotPreferredAddress() {
294 if (resolvedAddresses == null) {
295 return false;
296 }
297
298 final int size = resolvedAddresses.size();
299 switch (resolveAddressTypes[0]) {
300 case IPv4:
301 for (int i = 0; i < size; i ++) {
302 if (resolvedAddresses.get(i) instanceof Inet4Address) {
303 return true;
304 }
305 }
306 break;
307 case IPv6:
308 for (int i = 0; i < size; i ++) {
309 if (resolvedAddresses.get(i) instanceof Inet6Address) {
310 return true;
311 }
312 }
313 break;
314 }
315
316 return false;
317 }
318
319 private void finishResolve() {
320 if (!queriesInProgress.isEmpty()) {
321
322 for (Iterator<Future<DnsResponse>> i = queriesInProgress.iterator(); i.hasNext();) {
323 Future<DnsResponse> f = i.next();
324 i.remove();
325
326 if (!f.cancel(false)) {
327 f.addListener(RELEASE_RESPONSE);
328 }
329 }
330 }
331
332 if (resolvedAddresses != null) {
333
334 for (InternetProtocolFamily f: resolveAddressTypes) {
335 switch (f) {
336 case IPv4:
337 if (finishResolveWithIPv4()) {
338 return;
339 }
340 break;
341 case IPv6:
342 if (finishResolveWithIPv6()) {
343 return;
344 }
345 break;
346 }
347 }
348 }
349
350
351 int tries = maxAllowedQueries - allowedQueries;
352 UnknownHostException cause;
353 if (tries > 1) {
354 cause = new UnknownHostException(
355 "failed to resolve " + hostname + " after " + tries + " queries:" +
356 trace);
357 } else {
358 cause = new UnknownHostException("failed to resolve " + hostname + ':' + trace);
359 }
360
361 promise.tryFailure(cause);
362 }
363
364 private boolean finishResolveWithIPv4() {
365 final List<InetAddress> resolvedAddresses = this.resolvedAddresses;
366 final int size = resolvedAddresses.size();
367
368 for (int i = 0; i < size; i ++) {
369 InetAddress a = resolvedAddresses.get(i);
370 if (a instanceof Inet4Address) {
371 promise.trySuccess(new InetSocketAddress(a, port));
372 return true;
373 }
374 }
375
376 return false;
377 }
378
379 private boolean finishResolveWithIPv6() {
380 final List<InetAddress> resolvedAddresses = this.resolvedAddresses;
381 final int size = resolvedAddresses.size();
382
383 for (int i = 0; i < size; i ++) {
384 InetAddress a = resolvedAddresses.get(i);
385 if (a instanceof Inet6Address) {
386 promise.trySuccess(new InetSocketAddress(a, port));
387 return true;
388 }
389 }
390
391 return false;
392 }
393
394
395
396
397 static String decodeDomainName(ByteBuf buf) {
398 buf.markReaderIndex();
399 try {
400 int position = -1;
401 int checked = 0;
402 int length = buf.writerIndex();
403 StringBuilder name = new StringBuilder(64);
404 for (int len = buf.readUnsignedByte(); buf.isReadable() && len != 0; len = buf.readUnsignedByte()) {
405 boolean pointer = (len & 0xc0) == 0xc0;
406 if (pointer) {
407 if (position == -1) {
408 position = buf.readerIndex() + 1;
409 }
410 buf.readerIndex((len & 0x3f) << 8 | buf.readUnsignedByte());
411
412 checked += 2;
413 if (checked >= length) {
414
415 return null;
416 }
417 } else {
418 name.append(buf.toString(buf.readerIndex(), len, CharsetUtil.UTF_8)).append('.');
419 buf.skipBytes(len);
420 }
421 }
422
423 if (position != -1) {
424 buf.readerIndex(position);
425 }
426
427 if (name.length() == 0) {
428 return null;
429 }
430
431 return name.substring(0, name.length() - 1);
432 } finally {
433 buf.resetReaderIndex();
434 }
435 }
436
437 private void followCname(
438 InetSocketAddress nameServerAddr, String name, String cname) {
439
440 if (trace == null) {
441 trace = new StringBuilder(128);
442 }
443
444 trace.append(StringUtil.NEWLINE);
445 trace.append("\tfrom ");
446 trace.append(nameServerAddr);
447 trace.append(": ");
448 trace.append(name);
449 trace.append(" CNAME ");
450 trace.append(cname);
451
452 query(parent.nameServerAddresses, new DnsQuestion(cname, DnsType.A, DnsClass.IN));
453 query(parent.nameServerAddresses, new DnsQuestion(cname, DnsType.AAAA, DnsClass.IN));
454 }
455
456 private void addTrace(InetSocketAddress nameServerAddr, String msg) {
457 if (trace == null) {
458 trace = new StringBuilder(128);
459 }
460
461 trace.append(StringUtil.NEWLINE);
462 trace.append("\tfrom ");
463 trace.append(nameServerAddr);
464 trace.append(": ");
465 trace.append(msg);
466 }
467
468 private void addTrace(Throwable cause) {
469 if (trace == null) {
470 trace = new StringBuilder(128);
471 }
472
473 trace.append(StringUtil.NEWLINE);
474 trace.append("Caused by: ");
475 trace.append(cause);
476 }
477 }