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