/* This file is part of GNUnet. (C) 2011, 2012 Christian Grothoff (and other contributing authors) GNUnet is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNUnet is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNUnet; see the file COPYING. If not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.gnunet.util; import com.google.common.net.InetAddresses; import org.gnunet.construct.*; import org.gnunet.construct.ProtocolViolationException; import org.gnunet.util.getopt.Argument; import org.gnunet.util.getopt.ArgumentAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.LinkedList; /** * Resolve hostnames asynchronously, using the gnunet resolver service if necessary. *

* TODO: implement reverse lookup (already done in the C-API) */ public class Resolver { private static final Logger logger = LoggerFactory .getLogger(Resolver.class); private static Resolver singletonInstance; private Configuration cfg; private Client client; public static InetAddress getInetAddressFromString(String ipString) { try { return InetAddresses.forString(ipString); } catch (IllegalArgumentException e) { return null; } } @UnionCase(4) public static class GetMessage implements GnunetMessage.Body { static final int DIRECTION_GET_IP = 0; static final int DIRECTION_GET_NAME = 1; static final int AF_UNSPEC = 0; static final int AF_INET = 2; static final int AF_INET6 = 10; @UInt32 public int direction; @UInt32 public int domain; @Union(tag = "direction", optional = true) public Address addr; } public interface Address extends MessageUnion { } @UnionCase(GetMessage.DIRECTION_GET_IP) public static class TextualAddress implements Address { @ZeroTerminatedString public String addr; } @UnionCase(GetMessage.DIRECTION_GET_NAME) public static class NumericAddress implements Address { @FillWith @UInt8 public byte[] addr; } @UnionCase(5) public static class ResolverResponse implements GnunetMessage.Body { @NestedMessage(optional = true) public ResponseBody responseBody; } public static class ResponseBody implements Message { @FillWith @UInt8 public byte[] addr; } /** * Callback object for hostname resolution. */ public interface AddressCallback { /** * Called for every address the requested hostname resolves to. * * @param addr address for the resolved name */ public void onAddress(InetAddress addr); /** * Called after every result (if any) has been passed to onAddress. */ public void onFinished(); /** * Called when the resolve operation times out before returning every result. */ void onTimeout(); } /** * Configuration to use with the Resolver. *

* Usually called by the entry points Program/Service. * * @param cfg configuration to use */ public void setConfiguration(Configuration cfg) { this.cfg = cfg; } private void lazyConnect() { if (client == null) { if (cfg == null) { throw new AssertionError("Resolver has no Configuration"); } client = new Client("resolver", cfg); } } private InetAddress getInet4Localhost() { try { return InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); } catch (UnknownHostException e) { throw new RuntimeException(); } } private InetAddress getInet6Localhost() { try { return InetAddress.getByAddress(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}); } catch (UnknownHostException e) { throw new RuntimeException(); } } public class ResolveHandle implements Cancelable { private String hostname; private AbsoluteTime deadline; private AddressCallback cb; private boolean finished = false; private boolean canceled = false; private Cancelable transmitTask = null; private Cancelable receiveTask = null; public void cancel() { if (finished) { throw new AssertionError("Resolve already finished"); } if (canceled) { throw new AssertionError("ResolveHandle canceled twice"); } if (queuedRequests.contains(this)) { queuedRequests.remove(this); } else { if (receiveTask != null) { receiveTask.cancel(); } if (transmitTask != null) { transmitTask.cancel(); } } canceled = true; } } private LinkedList queuedRequests = new LinkedList(); private boolean resolveActive = false; /** * Resolve the hostname 'hostname'. * * @param hostname hostname to resolve * @param timeout timeout, calls cb.onTimeout on expiratoin * @param cb callback * @return a handle to cancel the request, null if request could be satisfied immediately */ public Cancelable resolveHostname(String hostname, RelativeTime timeout, final AddressCallback cb) { // try if hostname is numeric IP or loopback if (hostname.equalsIgnoreCase("localhost")) { logger.debug("resolving address locally"); cb.onAddress(getInet6Localhost()); cb.onAddress(getInet4Localhost()); cb.onFinished(); return null; } if (hostname.equalsIgnoreCase("ip6-localhost")) { cb.onAddress(getInet6Localhost()); cb.onFinished(); return null; } InetAddress inetAddr = getInetAddressFromString(hostname); if (inetAddr != null) { cb.onAddress(inetAddr); cb.onFinished(); return null; } final ResolveHandle rh = new ResolveHandle(); rh.hostname = hostname; rh.deadline = timeout.toAbsolute(); rh.cb = cb; queuedRequests.addLast(rh); handleNextRequest(); return rh; } private void handleNextRequest() { if (!resolveActive && !queuedRequests.isEmpty()) { ResolveHandle rh = queuedRequests.pollFirst(); handleRequest(rh); } } private void handleRequest(final ResolveHandle rh) { if (resolveActive) { throw new AssertionError("resolveActive but new resolve started"); } resolveActive = true; lazyConnect(); final GetMessage req = new GetMessage(); req.direction = GetMessage.DIRECTION_GET_IP; req.domain = GetMessage.AF_UNSPEC; TextualAddress textAddr = new TextualAddress(); textAddr.addr = rh.hostname; req.addr = textAddr; final AbsoluteTime deadline = rh.deadline; logger.debug("deadline is " + deadline + " | now is " + AbsoluteTime.now()); logger.debug("remaining is " + deadline.getRemaining()); rh.transmitTask = client.notifyTransmitReady( deadline.getRemaining(), true, 0, new MessageTransmitter() { @Override public void transmit(Connection.MessageSink sink) { if (sink == null) { onTimeout(rh); } sink.send(req); rh.transmitTask = null; logger.debug("recv in notifyTransmitReady cb"); rh.receiveTask = client.receive(deadline.getRemaining(), new MessageReceiver() { @Override public void process(GnunetMessage.Body msg) { rh.receiveTask = null; ResolverResponse gmsg = (ResolverResponse) msg; if (gmsg.responseBody != null) { try { InetAddress in_addr; int len = gmsg.responseBody.addr.length; if (len == 4 || len == 16) { in_addr = InetAddress.getByAddress(gmsg.responseBody.addr); } else { throw new ProtocolViolationException("malformed address message"); } rh.cb.onAddress(in_addr); rh.receiveTask = client.receive(deadline.getRemaining(), this); } catch (UnknownHostException e) { throw new ProtocolViolationException("malformed address"); } } else { resolveActive = false; rh.cb.onFinished(); handleNextRequest(); } } @Override public void handleError() { onTimeout(rh); } }); } @Override public void handleError() { throw new RuntimeException("unexpected transmit error"); } }); } private void onTimeout(ResolveHandle h) { resolveActive = false; h.cb.onTimeout(); handleNextRequest(); } public static Resolver getInstance() { if (singletonInstance == null) { singletonInstance = new Resolver(); } return singletonInstance; } /** * Return a textual representation of an InetAddress. Shortens IPv6 addresses. * * @param addr the address to convert * @return textual representation of the address */ public static String ipToString(InetAddress addr) { byte[] a = addr.getAddress(); if (a.length == 4) { return addr.getHostAddress(); } else if (a.length == 16) { String s = addr.getHostAddress(); // replace the first group of zeroes (not the longest) with :: return s.replaceFirst("[:]?0[:](0[:])+0?", "::"); } else { throw new RuntimeException("unknown InetAddress format"); } } public static void main(final String[] argv) { new Program(argv) { @Argument(shortname = "r", longname = "reverse", description = "do reverse dns lookup", action = ArgumentAction.SET) boolean isReverse; @Override public void run() { if (isReverse) { System.out.println("reverse lookup not supported"); } else { resolve(); } } public void resolve() { final RelativeTime timeout = RelativeTime.SECOND; if (unprocessedArgs.length == 0) { logger.warn("no hostname(s) given"); } else { logger.info("resolving hostname '" + unprocessedArgs[0] + "'"); Resolver.getInstance().resolveHostname(unprocessedArgs[0], timeout, new AddressCallback() { int next = 1; @Override public void onAddress(InetAddress addr) { System.out.println(ipToString(addr)); } @Override public void onFinished() { logger.info("resolve finished"); next(); } @Override public void onTimeout() { logger.warn("resolve timed out"); next(); } public void next() { if (unprocessedArgs.length > next) { logger.info("resolving hostname '" + unprocessedArgs[next] + "'"); Resolver.getInstance().resolveHostname(unprocessedArgs[next], timeout, this); next++; } } }); } } @Override protected String makeHelpText() { return "tool for forward and reverse DNS lookup"; } }.start(); } }