/*
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();
}
}