// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
// Parts of this are derived from lib/dns/xfrin.c from BIND 9; its copyright
// notice follows.
/*
* Copyright (C) 1999-2001 Internet Software Consortium.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM
* DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
* INTERNET SOFTWARE CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
* FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
* WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package org.xbill.DNS;
import java.io.*;
import java.net.*;
import java.util.*;
/**
* An incoming DNS Zone Transfer. To use this class, first initialize an
* object, then call the run() method. If run() doesn't throw an exception
* the result will either be an IXFR-style response, an AXFR-style response,
* or an indication that the zone is up to date.
*
* @author Brian Wellington
*/
public class ZoneTransferIn {
private static final int INITIALSOA = 0;
private static final int FIRSTDATA = 1;
private static final int IXFR_DELSOA = 2;
private static final int IXFR_DEL = 3;
private static final int IXFR_ADDSOA = 4;
private static final int IXFR_ADD = 5;
private static final int AXFR = 6;
private static final int END = 7;
private Name zname;
private int qtype;
private int dclass;
private long ixfr_serial;
private boolean want_fallback;
private ZoneTransferHandler handler;
private SocketAddress localAddress;
private SocketAddress address;
private TCPClient client;
private TSIG tsig;
private TSIG.StreamVerifier verifier;
private long timeout = 900 * 1000;
private int state;
private long end_serial;
private long current_serial;
private Record initialsoa;
private int rtype;
public static class Delta {
/**
* All changes between two versions of a zone in an IXFR response.
*/
/** The starting serial number of this delta. */
public long start;
/** The ending serial number of this delta. */
public long end;
/** A list of records added between the start and end versions */
public List adds;
/** A list of records deleted between the start and end versions */
public List deletes;
private
Delta() {
adds = new ArrayList();
deletes = new ArrayList();
}
}
public static interface ZoneTransferHandler {
/**
* Handles a Zone Transfer.
*/
/**
* Called when an AXFR transfer begins.
*/
public void startAXFR() throws ZoneTransferException;
/**
* Called when an IXFR transfer begins.
*/
public void startIXFR() throws ZoneTransferException;
/**
* Called when a series of IXFR deletions begins.
* @param soa The starting SOA.
*/
public void startIXFRDeletes(Record soa) throws ZoneTransferException;
/**
* Called when a series of IXFR adds begins.
* @param soa The starting SOA.
*/
public void startIXFRAdds(Record soa) throws ZoneTransferException;
/**
* Called for each content record in an AXFR.
* @param r The DNS record.
*/
public void handleRecord(Record r) throws ZoneTransferException;
};
private static class BasicHandler implements ZoneTransferHandler {
private List axfr;
private List ixfr;
public void startAXFR() {
axfr = new ArrayList();
}
public void startIXFR() {
ixfr = new ArrayList();
}
public void startIXFRDeletes(Record soa) {
Delta delta = new Delta();
delta.deletes.add(soa);
delta.start = getSOASerial(soa);
ixfr.add(delta);
}
public void startIXFRAdds(Record soa) {
Delta delta = (Delta) ixfr.get(ixfr.size() - 1);
delta.adds.add(soa);
delta.end = getSOASerial(soa);
}
public void handleRecord(Record r) {
List list;
if (ixfr != null) {
Delta delta = (Delta) ixfr.get(ixfr.size() - 1);
if (delta.adds.size() > 0)
list = delta.adds;
else
list = delta.deletes;
} else
list = axfr;
list.add(r);
}
};
private
ZoneTransferIn() {}
private
ZoneTransferIn(Name zone, int xfrtype, long serial, boolean fallback,
SocketAddress address, TSIG key)
{
this.address = address;
this.tsig = key;
if (zone.isAbsolute())
zname = zone;
else {
try {
zname = Name.concatenate(zone, Name.root);
}
catch (NameTooLongException e) {
throw new IllegalArgumentException("ZoneTransferIn: " +
"name too long");
}
}
qtype = xfrtype;
dclass = DClass.IN;
ixfr_serial = serial;
want_fallback = fallback;
state = INITIALSOA;
}
/**
* Instantiates a ZoneTransferIn object to do an AXFR (full zone transfer).
* @param zone The zone to transfer.
* @param address The host/port from which to transfer the zone.
* @param key The TSIG key used to authenticate the transfer, or null.
* @return The ZoneTransferIn object.
* @throws UnknownHostException The host does not exist.
*/
public static ZoneTransferIn
newAXFR(Name zone, SocketAddress address, TSIG key) {
return new ZoneTransferIn(zone, Type.AXFR, 0, false, address, key);
}
/**
* Instantiates a ZoneTransferIn object to do an AXFR (full zone transfer).
* @param zone The zone to transfer.
* @param host The host from which to transfer the zone.
* @param port The port to connect to on the server, or 0 for the default.
* @param key The TSIG key used to authenticate the transfer, or null.
* @return The ZoneTransferIn object.
* @throws UnknownHostException The host does not exist.
*/
public static ZoneTransferIn
newAXFR(Name zone, String host, int port, TSIG key)
throws UnknownHostException
{
if (port == 0)
port = SimpleResolver.DEFAULT_PORT;
return newAXFR(zone, new InetSocketAddress(host, port), key);
}
/**
* Instantiates a ZoneTransferIn object to do an AXFR (full zone transfer).
* @param zone The zone to transfer.
* @param host The host from which to transfer the zone.
* @param key The TSIG key used to authenticate the transfer, or null.
* @return The ZoneTransferIn object.
* @throws UnknownHostException The host does not exist.
*/
public static ZoneTransferIn
newAXFR(Name zone, String host, TSIG key)
throws UnknownHostException
{
return newAXFR(zone, host, 0, key);
}
/**
* Instantiates a ZoneTransferIn object to do an IXFR (incremental zone
* transfer).
* @param zone The zone to transfer.
* @param serial The existing serial number.
* @param fallback If true, fall back to AXFR if IXFR is not supported.
* @param address The host/port from which to transfer the zone.
* @param key The TSIG key used to authenticate the transfer, or null.
* @return The ZoneTransferIn object.
* @throws UnknownHostException The host does not exist.
*/
public static ZoneTransferIn
newIXFR(Name zone, long serial, boolean fallback, SocketAddress address,
TSIG key)
{
return new ZoneTransferIn(zone, Type.IXFR, serial, fallback, address,
key);
}
/**
* Instantiates a ZoneTransferIn object to do an IXFR (incremental zone
* transfer).
* @param zone The zone to transfer.
* @param serial The existing serial number.
* @param fallback If true, fall back to AXFR if IXFR is not supported.
* @param host The host from which to transfer the zone.
* @param port The port to connect to on the server, or 0 for the default.
* @param key The TSIG key used to authenticate the transfer, or null.
* @return The ZoneTransferIn object.
* @throws UnknownHostException The host does not exist.
*/
public static ZoneTransferIn
newIXFR(Name zone, long serial, boolean fallback, String host, int port,
TSIG key)
throws UnknownHostException
{
if (port == 0)
port = SimpleResolver.DEFAULT_PORT;
return newIXFR(zone, serial, fallback,
new InetSocketAddress(host, port), key);
}
/**
* Instantiates a ZoneTransferIn object to do an IXFR (incremental zone
* transfer).
* @param zone The zone to transfer.
* @param serial The existing serial number.
* @param fallback If true, fall back to AXFR if IXFR is not supported.
* @param host The host from which to transfer the zone.
* @param key The TSIG key used to authenticate the transfer, or null.
* @return The ZoneTransferIn object.
* @throws UnknownHostException The host does not exist.
*/
public static ZoneTransferIn
newIXFR(Name zone, long serial, boolean fallback, String host, TSIG key)
throws UnknownHostException
{
return newIXFR(zone, serial, fallback, host, 0, key);
}
/**
* Gets the name of the zone being transferred.
*/
public Name
getName() {
return zname;
}
/**
* Gets the type of zone transfer (either AXFR or IXFR).
*/
public int
getType() {
return qtype;
}
/**
* Sets a timeout on this zone transfer. The default is 900 seconds (15
* minutes).
* @param secs The maximum amount of time that this zone transfer can take.
*/
public void
setTimeout(int secs) {
if (secs < 0)
throw new IllegalArgumentException("timeout cannot be " +
"negative");
timeout = 1000L * secs;
}
/**
* Sets an alternate DNS class for this zone transfer.
* @param dclass The class to use instead of class IN.
*/
public void
setDClass(int dclass) {
DClass.check(dclass);
this.dclass = dclass;
}
/**
* Sets the local address to bind to when sending messages.
* @param addr The local address to send messages from.
*/
public void
setLocalAddress(SocketAddress addr) {
this.localAddress = addr;
}
private void
openConnection() throws IOException {
long endTime = System.currentTimeMillis() + timeout;
client = new TCPClient(endTime);
if (localAddress != null)
client.bind(localAddress);
client.connect(address);
}
private void
sendQuery() throws IOException {
Record question = Record.newRecord(zname, qtype, dclass);
Message query = new Message();
query.getHeader().setOpcode(Opcode.QUERY);
query.addRecord(question, Section.QUESTION);
if (qtype == Type.IXFR) {
Record soa = new SOARecord(zname, dclass, 0, Name.root,
Name.root, ixfr_serial,
0, 0, 0, 0);
query.addRecord(soa, Section.AUTHORITY);
}
if (tsig != null) {
tsig.apply(query, null);
verifier = new TSIG.StreamVerifier(tsig, query.getTSIG());
}
byte [] out = query.toWire(Message.MAXLENGTH);
client.send(out);
}
private static long
getSOASerial(Record rec) {
SOARecord soa = (SOARecord) rec;
return soa.getSerial();
}
private void
logxfr(String s) {
if (Options.check("verbose"))
System.out.println(zname + ": " + s);
}
private void
fail(String s) throws ZoneTransferException {
throw new ZoneTransferException(s);
}
private void
fallback() throws ZoneTransferException {
if (!want_fallback)
fail("server doesn't support IXFR");
logxfr("falling back to AXFR");
qtype = Type.AXFR;
state = INITIALSOA;
}
private void
parseRR(Record rec) throws ZoneTransferException {
int type = rec.getType();
Delta delta;
switch (state) {
case INITIALSOA:
if (type != Type.SOA)
fail("missing initial SOA");
initialsoa = rec;
// Remember the serial number in the initial SOA; we need it
// to recognize the end of an IXFR.
end_serial = getSOASerial(rec);
if (qtype == Type.IXFR &&
Serial.compare(end_serial, ixfr_serial) <= 0)
{
logxfr("up to date");
state = END;
break;
}
state = FIRSTDATA;
break;
case FIRSTDATA:
// If the transfer begins with 1 SOA, it's an AXFR.
// If it begins with 2 SOAs, it's an IXFR.
if (qtype == Type.IXFR && type == Type.SOA &&
getSOASerial(rec) == ixfr_serial)
{
rtype = Type.IXFR;
handler.startIXFR();
logxfr("got incremental response");
state = IXFR_DELSOA;
} else {
rtype = Type.AXFR;
handler.startAXFR();
handler.handleRecord(initialsoa);
logxfr("got nonincremental response");
state = AXFR;
}
parseRR(rec); // Restart...
return;
case IXFR_DELSOA:
handler.startIXFRDeletes(rec);
state = IXFR_DEL;
break;
case IXFR_DEL:
if (type == Type.SOA) {
current_serial = getSOASerial(rec);
state = IXFR_ADDSOA;
parseRR(rec); // Restart...
return;
}
handler.handleRecord(rec);
break;
case IXFR_ADDSOA:
handler.startIXFRAdds(rec);
state = IXFR_ADD;
break;
case IXFR_ADD:
if (type == Type.SOA) {
long soa_serial = getSOASerial(rec);
if (soa_serial == end_serial) {
state = END;
break;
} else if (soa_serial != current_serial) {
fail("IXFR out of sync: expected serial " +
current_serial + " , got " + soa_serial);
} else {
state = IXFR_DELSOA;
parseRR(rec); // Restart...
return;
}
}
handler.handleRecord(rec);
break;
case AXFR:
// Old BINDs sent cross class A records for non IN classes.
if (type == Type.A && rec.getDClass() != dclass)
break;
handler.handleRecord(rec);
if (type == Type.SOA) {
state = END;
}
break;
case END:
fail("extra data");
break;
default:
fail("invalid state");
break;
}
}
private void
closeConnection() {
try {
if (client != null)
client.cleanup();
}
catch (IOException e) {
}
}
private Message
parseMessage(byte [] b) throws WireParseException {
try {
return new Message(b);
}
catch (IOException e) {
if (e instanceof WireParseException)
throw (WireParseException) e;
throw new WireParseException("Error parsing message");
}
}
private void
doxfr() throws IOException, ZoneTransferException {
sendQuery();
while (state != END) {
byte [] in = client.recv();
Message response = parseMessage(in);
if (response.getHeader().getRcode() == Rcode.NOERROR &&
verifier != null)
{
TSIGRecord tsigrec = response.getTSIG();
int error = verifier.verify(response, in);
if (error != Rcode.NOERROR)
fail("TSIG failure");
}
Record [] answers = response.getSectionArray(Section.ANSWER);
if (state == INITIALSOA) {
int rcode = response.getRcode();
if (rcode != Rcode.NOERROR) {
if (qtype == Type.IXFR &&
rcode == Rcode.NOTIMP)
{
fallback();
doxfr();
return;
}
fail(Rcode.string(rcode));
}
Record question = response.getQuestion();
if (question != null && question.getType() != qtype) {
fail("invalid question section");
}
if (answers.length == 0 && qtype == Type.IXFR) {
fallback();
doxfr();
return;
}
}
for (int i = 0; i < answers.length; i++) {
parseRR(answers[i]);
}
if (state == END && verifier != null &&
!response.isVerified())
fail("last message must be signed");
}
}
/**
* Does the zone transfer.
* @param handler The callback object that handles the zone transfer data.
* @throws IOException The zone transfer failed to due an IO problem.
* @throws ZoneTransferException The zone transfer failed to due a problem
* with the zone transfer itself.
*/
public void
run(ZoneTransferHandler handler) throws IOException, ZoneTransferException {
this.handler = handler;
try {
openConnection();
doxfr();
}
finally {
closeConnection();
}
}
/**
* Does the zone transfer.
* @return A list, which is either an AXFR-style response (List of Records),
* and IXFR-style response (List of Deltas), or null, which indicates that
* an IXFR was performed and the zone is up to date.
* @throws IOException The zone transfer failed to due an IO problem.
* @throws ZoneTransferException The zone transfer failed to due a problem
* with the zone transfer itself.
*/
public List
run() throws IOException, ZoneTransferException {
BasicHandler handler = new BasicHandler();
run(handler);
if (handler.axfr != null)
return handler.axfr;
return handler.ixfr;
}
private BasicHandler
getBasicHandler() throws IllegalArgumentException {
if (handler instanceof BasicHandler)
return (BasicHandler) handler;
throw new IllegalArgumentException("ZoneTransferIn used callback " +
"interface");
}
/**
* Returns true if the response is an AXFR-style response (List of Records).
* This will be true if either an IXFR was performed, an IXFR was performed
* and the server provided a full zone transfer, or an IXFR failed and
* fallback to AXFR occurred.
*/
public boolean
isAXFR() {
return (rtype == Type.AXFR);
}
/**
* Gets the AXFR-style response.
* @throws IllegalArgumentException The transfer used the callback interface,
* so the response was not stored.
*/
public List
getAXFR() {
BasicHandler handler = getBasicHandler();
return handler.axfr;
}
/**
* Returns true if the response is an IXFR-style response (List of Deltas).
* This will be true only if an IXFR was performed and the server provided
* an incremental zone transfer.
*/
public boolean
isIXFR() {
return (rtype == Type.IXFR);
}
/**
* Gets the IXFR-style response.
* @throws IllegalArgumentException The transfer used the callback interface,
* so the response was not stored.
*/
public List
getIXFR() {
BasicHandler handler = getBasicHandler();
return handler.ixfr;
}
/**
* Returns true if the response indicates that the zone is up to date.
* This will be true only if an IXFR was performed.
* @throws IllegalArgumentException The transfer used the callback interface,
* so the response was not stored.
*/
public boolean
isCurrent() {
BasicHandler handler = getBasicHandler();
return (handler.axfr == null && handler.ixfr == null);
}
}