/* * Copyright (c) 2011-2013 The original author or authors * ------------------------------------------------------ * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. */ package io.vertx.core.dns.impl; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.handler.codec.dns.DatagramDnsQuery; import io.netty.handler.codec.dns.DatagramDnsQueryEncoder; import io.netty.handler.codec.dns.DatagramDnsResponseDecoder; import io.netty.handler.codec.dns.DefaultDnsQuestion; import io.netty.handler.codec.dns.DnsRecord; import io.netty.handler.codec.dns.DnsRecordType; import io.netty.handler.codec.dns.DnsResponse; import io.netty.handler.codec.dns.DnsSection; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.dns.DnsClient; import io.vertx.core.dns.DnsException; import io.vertx.core.dns.DnsResponseCode; import io.vertx.core.dns.MxRecord; import io.vertx.core.dns.SrvRecord; import io.vertx.core.dns.impl.decoder.RecordDecoder; import io.vertx.core.impl.ContextImpl; import io.vertx.core.impl.VertxInternal; import io.vertx.core.net.impl.PartialPooledByteBufAllocator; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; /** * @author <a href="mailto:nmaurer@redhat.com">Norman Maurer</a> */ public final class DnsClientImpl implements DnsClient { private static final char[] HEX_TABLE = "0123456789abcdef".toCharArray(); private final Bootstrap bootstrap; private final InetSocketAddress dnsServer; private final ContextImpl actualCtx; public DnsClientImpl(VertxInternal vertx, int port, String host) { ContextImpl creatingContext = vertx.getContext(); if (creatingContext != null && creatingContext.isMultiThreadedWorkerContext()) { throw new IllegalStateException("Cannot use DnsClient in a multi-threaded worker verticle"); } this.dnsServer = new InetSocketAddress(host, port); actualCtx = vertx.getOrCreateContext(); bootstrap = new Bootstrap(); bootstrap.group(actualCtx.nettyEventLoop()); bootstrap.channel(NioDatagramChannel.class); bootstrap.option(ChannelOption.ALLOCATOR, PartialPooledByteBufAllocator.INSTANCE); bootstrap.handler(new ChannelInitializer<DatagramChannel>() { @Override protected void initChannel(DatagramChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new DatagramDnsQueryEncoder()); pipeline.addLast(new DatagramDnsResponseDecoder()); } }); } @Override public DnsClient lookup4(String name, Handler<AsyncResult<String>> handler) { lookupSingle(name, handler, DnsRecordType.A); return this; } @Override public DnsClient lookup6(String name, Handler<AsyncResult<String>> handler) { lookupSingle(name, handler, DnsRecordType.AAAA); return this; } @Override public DnsClient lookup(String name, Handler<AsyncResult<String>> handler) { lookupSingle(name, handler, DnsRecordType.A, DnsRecordType.AAAA); return this; } @Override public DnsClient resolveA(String name, Handler<AsyncResult<List<String>>> handler) { lookupList(name, handler, DnsRecordType.A); return this; } @Override public DnsClient resolveCNAME(String name, Handler<AsyncResult<List<String> >> handler) { lookupList(name, handler, DnsRecordType.CNAME); return this; } @Override public DnsClient resolveMX(String name, Handler<AsyncResult<List<MxRecord>>> handler) { lookupList(name, handler, DnsRecordType.MX); return this; } @Override public DnsClient resolveTXT(String name, Handler<AsyncResult<List<String>>> handler) { lookupList(name, new Handler<AsyncResult<List<String>>>() { @SuppressWarnings("unchecked") @Override public void handle(AsyncResult event) { if (event.failed()) { handler.handle(event); } else { List<String> txts = new ArrayList<>(); List<List<String>> records = (List<List<String>>) event.result(); for (List<String> txt: records) { txts.addAll(txt); } handler.handle(Future.succeededFuture(txts)); } } }, DnsRecordType.TXT); return this; } @Override public DnsClient resolvePTR(String name, Handler<AsyncResult<String>> handler) { lookupSingle(name, handler, DnsRecordType.PTR); return this; } @Override public DnsClient resolveAAAA(String name, Handler<AsyncResult<List<String>>> handler) { lookupList(name, handler, DnsRecordType.AAAA); return this; } @Override public DnsClient resolveNS(String name, Handler<AsyncResult<List<String>>> handler) { lookupList(name, handler, DnsRecordType.NS); return this; } @Override public DnsClient resolveSRV(String name, Handler<AsyncResult<List<SrvRecord>>> handler) { lookupList(name, handler, DnsRecordType.SRV); return this; } @Override public DnsClient reverseLookup(String address, Handler<AsyncResult<String>> handler) { // TODO: Check if the given address is a valid ip address before pass it to InetAddres.getByName(..) // This is need as otherwise it may try to perform a DNS lookup. // An other option would be to change address to be of type InetAddress. try { InetAddress inetAddress = InetAddress.getByName(address); byte[] addr = inetAddress.getAddress(); StringBuilder reverseName = new StringBuilder(64); if (inetAddress instanceof Inet4Address) { // reverse ipv4 address reverseName.append(addr[3] & 0xff).append(".") .append(addr[2]& 0xff).append(".") .append(addr[1]& 0xff).append(".") .append(addr[0]& 0xff); } else { // It is an ipv 6 address time to reverse it for (int i = 0; i < 16; i++) { reverseName.append(HEX_TABLE[(addr[15 - i] & 0xf)]); reverseName.append("."); reverseName.append(HEX_TABLE[(addr[15 - i] >> 4) & 0xf]); if (i != 15) { reverseName.append("."); } } } reverseName.append(".in-addr.arpa"); return resolvePTR(reverseName.toString(), handler); } catch (UnknownHostException e) { // Should never happen as we work with ip addresses as input // anyway just in case notify the handler actualCtx.runOnContext((v) -> handler.handle(Future.failedFuture(e))); } return this; } private <T> void lookupSingle(String name, Handler<AsyncResult<T>> handler, DnsRecordType... types) { this.<T>lookupList(name, ar -> handler.handle(ar.map(result -> result.isEmpty() ? null : result.get(0))), types); } @SuppressWarnings("unchecked") private <T> void lookupList(String name, Handler<AsyncResult<List<T>>> handler, DnsRecordType... types) { Future<List<T>> result = Future.future(); result.setHandler(handler); lookup(name, result, types); } @SuppressWarnings("unchecked") private <T> void lookup(String name, Future<List<T>> result, DnsRecordType... types) { Objects.requireNonNull(name, "no null name accepted"); bootstrap.connect(dnsServer).addListener(new RetryChannelFutureListener(result) { @Override public void onSuccess(ChannelFuture future) throws Exception { DatagramDnsQuery query = new DatagramDnsQuery(null, dnsServer, ThreadLocalRandom.current().nextInt()); for (DnsRecordType type: types) { query.addRecord(DnsSection.QUESTION, new DefaultDnsQuestion(name, type, DnsRecord.CLASS_IN)); } future.channel().writeAndFlush(query).addListener(new RetryChannelFutureListener(result) { @Override public void onSuccess(ChannelFuture future) throws Exception { future.channel().pipeline().addLast(new SimpleChannelInboundHandler<DnsResponse>() { @Override protected void channelRead0(ChannelHandlerContext ctx, DnsResponse msg) throws Exception { DnsResponseCode code = DnsResponseCode.valueOf(msg.code().intValue()); if (code == DnsResponseCode.NOERROR) { int count = msg.count(DnsSection.ANSWER); List<T> records = new ArrayList<>(count); for (int idx = 0;idx < count;idx++) { DnsRecord a = msg.recordAt(DnsSection.ANSWER, idx); T record = RecordDecoder.decode(a); if (isRequestedType(a.type(), types)) { records.add(record); } } if (records.size() > 0 && (records.get(0) instanceof MxRecordImpl || records.get(0) instanceof SrvRecordImpl)) { Collections.sort((List) records); } setResult(result, records); } else { setFailure(result, new DnsException(code)); } ctx.close(); } private boolean isRequestedType(DnsRecordType dnsRecordType, DnsRecordType[] types) { for (DnsRecordType t : types) { if (t.equals(dnsRecordType)) { return true; } } return false; } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { setFailure(result, cause); ctx.close(); } }); } }); } }); } @SuppressWarnings("unchecked") private <T> void setResult(Future<List<T>> r, List<T> result) { if (r.isComplete()) { return; } actualCtx.executeFromIO(() -> { if (result instanceof Throwable) { r.fail((Throwable) result); } else { r.complete(result); } }); } @SuppressWarnings("unchecked") private <T> void setFailure(Future<List<T>> r, Throwable err) { if (r.isComplete()) { return; } actualCtx.executeFromIO(() -> { r.fail(err); }); } private abstract class RetryChannelFutureListener implements ChannelFutureListener { private final Future result; RetryChannelFutureListener(final Future result) { this.result = result; } @Override public final void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { if (!result.isComplete()) { result.fail(future.cause()); } } else { onSuccess(future); } } protected abstract void onSuccess(ChannelFuture future) throws Exception; } }