// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
package org.xbill.DNS;
import java.util.*;
import java.io.*;
import java.net.*;
/**
* An implementation of Resolver that sends one query to one server.
* SimpleResolver handles TCP retries, transaction security (TSIG), and
* EDNS 0.
* @see Resolver
* @see TSIG
* @see OPTRecord
*
* @author Brian Wellington
*/
public class SimpleResolver implements Resolver {
/** The default port to send queries to */
public static final int DEFAULT_PORT = 53;
/** The default EDNS payload size */
public static final int DEFAULT_EDNS_PAYLOADSIZE = 1280;
private InetSocketAddress address;
private InetSocketAddress localAddress;
private boolean useTCP, ignoreTruncation;
private OPTRecord queryOPT;
private TSIG tsig;
private long timeoutValue = 10 * 1000;
private static final short DEFAULT_UDPSIZE = 512;
private static String defaultResolver = "localhost";
private static int uniqueID = 0;
/**
* Creates a SimpleResolver that will query the specified host
* @exception UnknownHostException Failure occurred while finding the host
*/
public
SimpleResolver(String hostname) throws UnknownHostException {
if (hostname == null) {
hostname = ResolverConfig.getCurrentConfig().server();
if (hostname == null)
hostname = defaultResolver;
}
InetAddress addr;
if (hostname.equals("0"))
addr = InetAddress.getLocalHost();
else
addr = InetAddress.getByName(hostname);
address = new InetSocketAddress(addr, DEFAULT_PORT);
}
/**
* Creates a SimpleResolver. The host to query is either found by using
* ResolverConfig, or the default host is used.
* @see ResolverConfig
* @exception UnknownHostException Failure occurred while finding the host
*/
public
SimpleResolver() throws UnknownHostException {
this(null);
}
/**
* Gets the destination address associated with this SimpleResolver.
* Messages sent using this SimpleResolver will be sent to this address.
* @return The destination address associated with this SimpleResolver.
*/
InetSocketAddress
getAddress() {
return address;
}
/** Sets the default host (initially localhost) to query */
public static void
setDefaultResolver(String hostname) {
defaultResolver = hostname;
}
public void
setPort(int port) {
address = new InetSocketAddress(address.getAddress(), port);
}
/**
* Sets the address of the server to communicate with.
* @param addr The address of the DNS server
*/
public void
setAddress(InetSocketAddress addr) {
address = addr;
}
/**
* Sets the address of the server to communicate with (on the default
* DNS port)
* @param addr The address of the DNS server
*/
public void
setAddress(InetAddress addr) {
address = new InetSocketAddress(addr, address.getPort());
}
/**
* Sets the local address to bind to when sending messages.
* @param addr The local address to send messages from.
*/
public void
setLocalAddress(InetSocketAddress addr) {
localAddress = addr;
}
/**
* Sets the local address to bind to when sending messages. A random port
* will be used.
* @param addr The local address to send messages from.
*/
public void
setLocalAddress(InetAddress addr) {
localAddress = new InetSocketAddress(addr, 0);
}
public void
setTCP(boolean flag) {
this.useTCP = flag;
}
public void
setIgnoreTruncation(boolean flag) {
this.ignoreTruncation = flag;
}
public void
setEDNS(int level, int payloadSize, int flags, List options) {
if (level != 0 && level != -1)
throw new IllegalArgumentException("invalid EDNS level - " +
"must be 0 or -1");
if (payloadSize == 0)
payloadSize = DEFAULT_EDNS_PAYLOADSIZE;
queryOPT = new OPTRecord(payloadSize, 0, level, flags, options);
}
public void
setEDNS(int level) {
setEDNS(level, 0, 0, null);
}
public void
setTSIGKey(TSIG key) {
tsig = key;
}
TSIG
getTSIGKey() {
return tsig;
}
public void
setTimeout(int secs, int msecs) {
timeoutValue = (long)secs * 1000 + msecs;
}
public void
setTimeout(int secs) {
setTimeout(secs, 0);
}
long
getTimeout() {
return timeoutValue;
}
private Message
parseMessage(byte [] b) throws WireParseException {
try {
return (new Message(b));
}
catch (IOException e) {
if (Options.check("verbose"))
e.printStackTrace();
if (!(e instanceof WireParseException))
e = new WireParseException("Error parsing message");
throw (WireParseException) e;
}
}
private void
verifyTSIG(Message query, Message response, byte [] b, TSIG tsig) {
if (tsig == null)
return;
int error = tsig.verify(response, b, query.getTSIG());
if (Options.check("verbose"))
System.err.println("TSIG verify: " + Rcode.TSIGstring(error));
}
private void
applyEDNS(Message query) {
if (queryOPT == null || query.getOPT() != null)
return;
query.addRecord(queryOPT, Section.ADDITIONAL);
}
private int
maxUDPSize(Message query) {
OPTRecord opt = query.getOPT();
if (opt == null)
return DEFAULT_UDPSIZE;
else
return opt.getPayloadSize();
}
/**
* Sends a message to a single server and waits for a response. No checking
* is done to ensure that the response is associated with the query.
* @param query The query to send.
* @return The response.
* @throws IOException An error occurred while sending or receiving.
*/
public Message
send(Message query) throws IOException {
if (Options.check("verbose"))
System.err.println("Sending to " +
address.getAddress().getHostAddress() +
":" + address.getPort());
if (query.getHeader().getOpcode() == Opcode.QUERY) {
Record question = query.getQuestion();
if (question != null && question.getType() == Type.AXFR)
return sendAXFR(query);
}
query = (Message) query.clone();
applyEDNS(query);
if (tsig != null)
tsig.apply(query, null);
byte [] out = query.toWire(Message.MAXLENGTH);
int udpSize = maxUDPSize(query);
boolean tcp = false;
long endTime = System.currentTimeMillis() + timeoutValue;
do {
byte [] in;
if (useTCP || out.length > udpSize)
tcp = true;
if (tcp)
in = TCPClient.sendrecv(localAddress, address, out,
endTime);
else
in = UDPClient.sendrecv(localAddress, address, out,
udpSize, endTime);
/*
* Check that the response is long enough.
*/
if (in.length < Header.LENGTH) {
throw new WireParseException("invalid DNS header - " +
"too short");
}
/*
* Check that the response ID matches the query ID. We want
* to check this before actually parsing the message, so that
* if there's a malformed response that's not ours, it
* doesn't confuse us.
*/
int id = ((in[0] & 0xFF) << 8) + (in[1] & 0xFF);
int qid = query.getHeader().getID();
if (id != qid) {
String error = "invalid message id: expected " + qid +
"; got id " + id;
if (tcp) {
throw new WireParseException(error);
} else {
if (Options.check("verbose")) {
System.err.println(error);
}
continue;
}
}
Message response = parseMessage(in);
verifyTSIG(query, response, in, tsig);
if (!tcp && !ignoreTruncation &&
response.getHeader().getFlag(Flags.TC))
{
tcp = true;
continue;
}
return response;
} while (true);
}
/**
* Asynchronously sends a message to a single server, registering a listener
* to receive a callback on success or exception. Multiple asynchronous
* lookups can be performed in parallel. Since the callback may be invoked
* before the function returns, external synchronization is necessary.
* @param query The query to send
* @param listener The object containing the callbacks.
* @return An identifier, which is also a parameter in the callback
*/
public Object
sendAsync(final Message query, final ResolverListener listener) {
final Object id;
synchronized (this) {
id = new Integer(uniqueID++);
}
Record question = query.getQuestion();
String qname;
if (question != null)
qname = question.getName().toString();
else
qname = "(none)";
String name = this.getClass() + ": " + qname;
Thread thread = new ResolveThread(this, query, id, listener);
thread.setName(name);
thread.setDaemon(true);
thread.start();
return id;
}
private Message
sendAXFR(Message query) throws IOException {
Name qname = query.getQuestion().getName();
ZoneTransferIn xfrin = ZoneTransferIn.newAXFR(qname, address, tsig);
xfrin.setTimeout((int)(getTimeout() / 1000));
xfrin.setLocalAddress(localAddress);
try {
xfrin.run();
}
catch (ZoneTransferException e) {
throw new WireParseException(e.getMessage());
}
List records = xfrin.getAXFR();
Message response = new Message(query.getHeader().getID());
response.getHeader().setFlag(Flags.AA);
response.getHeader().setFlag(Flags.QR);
response.addRecord(query.getQuestion(), Section.QUESTION);
Iterator it = records.iterator();
while (it.hasNext())
response.addRecord((Record)it.next(), Section.ANSWER);
return response;
}
}